diff --git a/.coveragerc b/.coveragerc index a264bde79a2..84ca187fb3a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -97,6 +97,9 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py + homeassistant/components/ihc/* + homeassistant/components/*/ihc.py + homeassistant/components/insteon_local.py homeassistant/components/*/insteon_local.py @@ -106,6 +109,9 @@ omit = homeassistant/components/ios.py homeassistant/components/*/ios.py + homeassistant/components/iota.py + homeassistant/components/*/iota.py + homeassistant/components/isy994.py homeassistant/components/*/isy994.py @@ -145,6 +151,9 @@ omit = homeassistant/components/modbus.py homeassistant/components/*/modbus.py + homeassistant/components/mychevy.py + homeassistant/components/*/mychevy.py + homeassistant/components/mysensors.py homeassistant/components/*/mysensors.py @@ -241,6 +250,9 @@ omit = homeassistant/components/volvooncall.py homeassistant/components/*/volvooncall.py + homeassistant/components/waterfurnace.py + homeassistant/components/*/waterfurnace.py + homeassistant/components/*/webostv.py homeassistant/components/wemo.py @@ -304,6 +316,7 @@ omit = homeassistant/components/camera/ring.py homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py + homeassistant/components/camera/xeoma.py homeassistant/components/camera/yi.py homeassistant/components/climate/econet.py homeassistant/components/climate/ephember.py @@ -318,6 +331,7 @@ omit = homeassistant/components/climate/radiotherm.py homeassistant/components/climate/sensibo.py homeassistant/components/climate/touchline.py + homeassistant/components/climate/venstar.py homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/knx.py @@ -327,7 +341,6 @@ omit = homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py - homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/automatic.py homeassistant/components/device_tracker/bbox.py homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -504,6 +517,7 @@ omit = homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/blockchain.py homeassistant/components/sensor/bme280.py + homeassistant/components/sensor/bme680.py homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/buienradar.py @@ -606,6 +620,7 @@ omit = homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/ted5000.py + homeassistant/components/sensor/teksavvy.py homeassistant/components/sensor/temper.py homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py diff --git a/.github/move.yml b/.github/move.yml new file mode 100644 index 00000000000..e041083c9ae --- /dev/null +++ b/.github/move.yml @@ -0,0 +1,13 @@ +# Configuration for move-issues - https://github.com/dessant/move-issues + +# Delete the command comment. Ignored when the comment also contains other content +deleteCommand: true +# Close the source issue after moving +closeSourceIssue: true +# Lock the source issue after moving +lockSourceIssue: false +# Set custom aliases for targets +# aliases: +# r: repo +# or: owner/repo + diff --git a/CODEOWNERS b/CODEOWNERS index 99c103b1298..9ec7ce0742c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,12 +32,15 @@ homeassistant/components/zone.py @home-assistant/core # To monitor non-pypi additions requirements_all.txt @andrey-git +# HomeAssistant developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker homeassistant/components/zwave/* @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave +homeassistant/components/hassio.py @home-assistant/hassio + # Indiviudal components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/camera/yi.py @bachya diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 20a4489da90..d603843f51f 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,9 +1,8 @@ """ -ADS Component. +Support for Automation Device Specification (ADS). For more details about this component, please refer to the documentation. https://home-assistant.io/components/ads/ - """ import threading import struct @@ -29,7 +28,6 @@ ADSTYPE_BOOL = 'bool' DOMAIN = 'ads' -# config variable names CONF_ADS_VAR = 'adsvar' CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' CONF_ADS_TYPE = 'adstype' @@ -47,10 +45,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ + vol.Required(CONF_ADS_TYPE): + vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]), + vol.Required(CONF_ADS_VALUE): cv.match_all, vol.Required(CONF_ADS_VAR): cv.string, - vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, - ADSTYPE_BYTE]), - vol.Required(CONF_ADS_VALUE): cv.match_all }) @@ -59,15 +57,12 @@ def setup(hass, config): import pyads conf = config[DOMAIN] - # get ads connection parameters from config net_id = conf.get(CONF_DEVICE) ip_address = conf.get(CONF_IP_ADDRESS) port = conf.get(CONF_PORT) - # create a new ads connection client = pyads.Connection(net_id, port, ip_address) - # add some constants to AdsHub AdsHub.ADS_TYPEMAP = { ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, @@ -81,16 +76,13 @@ def setup(hass, config): AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT AdsHub.ADSError = pyads.ADSError - # connect to ads client and try to connect try: ads = AdsHub(client) except pyads.pyads.ADSError: _LOGGER.error( - 'Could not connect to ADS host (netid=%s, port=%s)', net_id, port - ) + "Could not connect to ADS host (netid=%s, port=%s)", net_id, port) return False - # add ads hub to hass data collection, listen to shutdown hass.data[DATA_ADS] = ads hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown) @@ -107,43 +99,41 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name, - schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME - ) + schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME) return True -# tuple to hold data needed for notification +# Tuple to hold data needed for notification NotificationItem = namedtuple( 'NotificationItem', 'hnotify huser name plc_datatype callback' ) -class AdsHub: - """Representation of a PyADS connection.""" +class AdsHub(object): + """Representation of an ADS connection.""" def __init__(self, ads_client): - """Initialize the ADS Hub.""" + """Initialize the ADS hub.""" self._client = ads_client self._client.open() - # all ADS devices are registered here + # All ADS devices are registered here self._devices = [] self._notification_items = {} self._lock = threading.Lock() def shutdown(self, *args, **kwargs): """Shutdown ADS connection.""" - _LOGGER.debug('Shutting down ADS') + _LOGGER.debug("Shutting down ADS") for notification_item in self._notification_items.values(): self._client.del_device_notification( notification_item.hnotify, notification_item.huser ) _LOGGER.debug( - 'Deleting device notification %d, %d', - notification_item.hnotify, notification_item.huser - ) + "Deleting device notification %d, %d", + notification_item.hnotify, notification_item.huser) self._client.close() def register_device(self, device): @@ -167,33 +157,30 @@ class AdsHub: with self._lock: hnotify, huser = self._client.add_device_notification( - name, attr, self._device_notification_callback - ) + name, attr, self._device_notification_callback) hnotify = int(hnotify) _LOGGER.debug( - 'Added Device Notification %d for variable %s', hnotify, name - ) + "Added device notification %d for variable %s", hnotify, name) self._notification_items[hnotify] = NotificationItem( - hnotify, huser, name, plc_datatype, callback - ) + hnotify, huser, name, plc_datatype, callback) def _device_notification_callback(self, addr, notification, huser): """Handle device notifications.""" contents = notification.contents hnotify = int(contents.hNotification) - _LOGGER.debug('Received Notification %d', hnotify) + _LOGGER.debug("Received notification %d", hnotify) data = contents.data try: notification_item = self._notification_items[hnotify] except KeyError: - _LOGGER.debug('Unknown Device Notification handle: %d', hnotify) + _LOGGER.debug("Unknown device notification handle: %d", hnotify) return - # parse data to desired datatype + # Parse data to desired datatype if notification_item.plc_datatype == self.PLCTYPE_BOOL: value = bool(struct.unpack(' dt_util.utcnow() @property def code_format(self): - """One or more characters.""" + """Return one or more characters.""" return None if self._code is None else '.+' def alarm_disarm(self, code=None): @@ -250,6 +255,7 @@ class ManualAlarm(alarm.AlarmControlPanel): self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + """Update the state.""" if self._state == state: return diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 9e388806e73..ef12cbe365f 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -26,6 +26,8 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time +_LOGGER = logging.getLogger(__name__) + CONF_CODE_TEMPLATE = 'code_template' CONF_PAYLOAD_DISARM = 'payload_disarm' @@ -58,6 +60,7 @@ ATTR_POST_PENDING_STATE = 'post_pending_state' def _state_validator(config): + """Validate the state.""" config = copy.deepcopy(config) for state in SUPPORTED_PRETRIGGER_STATES: if CONF_DELAY_TIME not in config[state]: @@ -72,6 +75,7 @@ def _state_validator(config): def _state_schema(state): + """Validate the state.""" schema = {} if state in SUPPORTED_PRETRIGGER_STATES: schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( @@ -117,8 +121,6 @@ PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, }), _state_validator)) -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the manual MQTT alarm platform.""" @@ -150,11 +152,10 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, code_template, - disarm_after_trigger, - state_topic, command_topic, qos, - payload_disarm, payload_arm_home, payload_arm_away, - payload_arm_night, config): + def __init__(self, hass, name, code, code_template, disarm_after_trigger, + state_topic, command_topic, qos, payload_disarm, + payload_arm_home, payload_arm_away, payload_arm_night, + config): """Init the manual MQTT alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass @@ -219,23 +220,26 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): @property def _active_state(self): + """Get the current state.""" if self.state == STATE_ALARM_PENDING: return self._previous_state else: return self._state def _pending_time(self, state): + """Get the pending time.""" pending_time = self._pending_time_by_state[state] if state == STATE_ALARM_TRIGGERED: pending_time += self._delay_time_by_state[self._previous_state] return pending_time def _within_pending_time(self, state): + """Get if the action is in the pending time window.""" return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property def code_format(self): - """One or more characters.""" + """Return one or more characters.""" return None if self._code is None else '.+' def alarm_disarm(self, code=None): @@ -280,6 +284,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + """Update the state.""" if self._state == state: return @@ -329,7 +334,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): return state_attr def async_added_to_hass(self): - """Subscribe mqtt events. + """Subscribe to MQTT events. This method must be run in the event loop and returns a coroutine. """ @@ -358,5 +363,5 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): @asyncio.coroutine def _async_state_changed_listener(self, entity_id, old_state, new_state): """Publish state change to MQTT.""" - mqtt.async_publish(self.hass, self._state_topic, new_state.state, - self._qos, True) + mqtt.async_publish( + self.hass, self._state_topic, new_state.state, self._qos, True) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index a4559160e3b..1422136c405 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, -}) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @asyncio.coroutine diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 81a8b02cc64..ceb79c1dc7b 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -12,8 +12,8 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pynx584==0.4'] @@ -25,14 +25,14 @@ DEFAULT_NAME = 'NX584' DEFAULT_PORT = 5007 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the nx584 platform.""" + """Set up the NX584 platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -88,7 +88,7 @@ class NX584Alarm(alarm.AlarmControlPanel): self._state = STATE_UNKNOWN zones = [] except IndexError: - _LOGGER.error("nx584 reports no partitions") + _LOGGER.error("NX584 reports no partitions") self._state = STATE_UNKNOWN zones = [] diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 6115311f873..964047f91e9 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -8,9 +8,8 @@ import asyncio import logging import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.satel_integra import (CONF_ARM_HOME_MODE, - DATA_SATEL, - SIGNAL_PANEL_MESSAGE) +from homeassistant.components.satel_integra import ( + CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,12 +20,12 @@ DEPENDENCIES = ['satel_integra'] @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up for AlarmDecoder alarm panels.""" + """Set up for Satel Integra alarm panels.""" if not discovery_info: return - device = SatelIntegraAlarmPanel("Alarm Panel", - discovery_info.get(CONF_ARM_HOME_MODE)) + device = SatelIntegraAlarmPanel( + "Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)) async_add_devices([device]) @@ -47,7 +46,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): @callback def _message_callback(self, message): - + """Handle received messages.""" if message != self._state: self._state = message self.async_schedule_update_ha_state() @@ -90,5 +89,5 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: - yield from self.hass.data[DATA_SATEL].arm(code, - self._arm_home_mode) + yield from self.hass.data[DATA_SATEL].arm( + code, self._arm_home_mode) diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 7f4e4dfa756..3b991c5b236 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -11,9 +11,9 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_CODE, CONF_NAME, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - EVENT_HOMEASSISTANT_STOP) + CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['simplisafe-python==1.0.5'] @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'SimpliSafe' DOMAIN = 'simplisafe' + NOTIFICATION_ID = 'simplisafe_notification' NOTIFICATION_TITLE = 'SimpliSafe Setup' @@ -65,7 +66,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SimpliSafeAlarm(alarm.AlarmControlPanel): - """Representation a SimpliSafe alarm.""" + """Representation of a SimpliSafe alarm.""" def __init__(self, simplisafe, name, code): """Initialize the SimpliSafe alarm.""" @@ -82,7 +83,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """One or more characters if code is defined.""" + """Return one or more characters if code is defined.""" return None if self._code is None else '.+' @property @@ -103,12 +104,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" return { - 'temperature': self.simplisafe.temperature(), + 'alarm': self.simplisafe.alarm(), 'co': self.simplisafe.carbon_monoxide(), 'fire': self.simplisafe.fire(), - 'alarm': self.simplisafe.alarm(), + 'flood': self.simplisafe.flood(), 'last_event': self.simplisafe.last_event(), - 'flood': self.simplisafe.flood() + 'temperature': self.simplisafe.temperature(), } def update(self): diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 4d9c72df2f1..5d5b2284bab 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -9,26 +9,27 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.spc import ( - SpcWebGateway, ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY) + ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) - _LOGGER = logging.getLogger(__name__) -SPC_AREA_MODE_TO_STATE = {'0': STATE_ALARM_DISARMED, - '1': STATE_ALARM_ARMED_HOME, - '3': STATE_ALARM_ARMED_AWAY} +SPC_AREA_MODE_TO_STATE = { + '0': STATE_ALARM_DISARMED, + '1': STATE_ALARM_ARMED_HOME, + '3': STATE_ALARM_ARMED_AWAY, +} def _get_alarm_state(spc_mode): + """Get the alarm state.""" return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN) @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the SPC alarm control panel platform.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None): @@ -42,7 +43,7 @@ def async_setup_platform(hass, config, async_add_devices, class SpcAlarm(alarm.AlarmControlPanel): - """Represents the SPC alarm panel.""" + """Representation of the SPC alarm panel.""" def __init__(self, api, area): """Initialize the SPC alarm panel.""" @@ -57,7 +58,7 @@ class SpcAlarm(alarm.AlarmControlPanel): @asyncio.coroutine def async_added_to_hass(self): - """Calbback for init handlers.""" + """Call for adding new entities.""" self.hass.data[DATA_REGISTRY].register_alarm_device( self._area_id, self) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 66764f58c26..74d63b1fb9c 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -8,8 +8,8 @@ import logging from time import sleep import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS from homeassistant.components.verisure import HUB as hub -from homeassistant.components.verisure import (CONF_ALARM, CONF_CODE_DIGITS) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) @@ -43,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): """Representation of a Verisure alarm status.""" def __init__(self): - """Initalize the Verisure alarm panel.""" + """Initialize the Verisure alarm panel.""" self._state = STATE_UNKNOWN self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index 8bc2539f772..771d157efe0 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -8,11 +8,10 @@ import asyncio import logging import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import (STATE_UNKNOWN, - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY) -from homeassistant.components.wink import WinkDevice, DOMAIN +from homeassistant.components.wink import DOMAIN, WinkDevice +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -41,7 +40,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self) @property diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 65243aa83ce..b683f5cfc7c 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -10,18 +10,33 @@ import logging import voluptuous as vol from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import entityfilter +from . import flash_briefings, intent, smart_home from .const import ( - DOMAIN, CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL) -from . import flash_briefings, intent + CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, + CONF_FILTER, CONF_ENTITY_CONFIG) _LOGGER = logging.getLogger(__name__) +CONF_FLASH_BRIEFINGS = 'flash_briefings' +CONF_SMART_HOME = 'smart_home' DEPENDENCIES = ['http'] -CONF_FLASH_BRIEFINGS = 'flash_briefings' +ALEXA_ENTITY_SCHEMA = vol.Schema({ + vol.Optional(smart_home.CONF_DESCRIPTION): cv.string, + vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(smart_home.CONF_NAME): cv.string, +}) +SMART_HOME_SCHEMA = vol.Schema({ + vol.Optional( + CONF_FILTER, + default=lambda: entityfilter.generate_filter([], [], [], []) + ): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: { @@ -33,7 +48,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_TEXT, default=""): cv.template, vol.Optional(CONF_DISPLAY_URL): cv.template, }]), - } + }, + # vol.Optional here would mean we couldn't distinguish between an empty + # smart_home: and none at all. + CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None), } }, extra=vol.ALLOW_EXTRA) @@ -49,4 +67,12 @@ def async_setup(hass, config): if flash_briefings_config: flash_briefings.async_setup(hass, flash_briefings_config) + try: + smart_home_config = config[CONF_SMART_HOME] + except KeyError: + pass + else: + smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) + smart_home.async_setup(hass, smart_home_config) + return True diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index c243fc12d5e..7d6489b535a 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -8,6 +8,9 @@ CONF_AUDIO = 'audio' CONF_TEXT = 'text' CONF_DISPLAY_URL = 'display_url' +CONF_FILTER = 'filter' +CONF_ENTITY_CONFIG = 'entity_config' + ATTR_UID = 'uid' ATTR_UPDATE_DATE = 'updateDate' ATTR_TITLE_TEXT = 'titleText' diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index ec7e3521c0a..02f47b05617 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -5,19 +5,18 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ import copy -import logging from datetime import datetime +import logging import uuid +from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template -from homeassistant.components import http from .const import ( - CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL, ATTR_UID, - ATTR_UPDATE_DATE, ATTR_TITLE_TEXT, ATTR_STREAM_URL, ATTR_MAIN_TEXT, - ATTR_REDIRECTION_URL, DATE_FORMAT) - + ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT, + ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, + CONF_TITLE, CONF_UID, DATE_FORMAT) _LOGGER = logging.getLogger(__name__) @@ -46,11 +45,11 @@ class AlexaFlashBriefingView(http.HomeAssistantView): @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" - _LOGGER.debug('Received Alexa flash briefing request for: %s', + _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) if self.flash_briefings.get(briefing_id) is None: - err = 'No configured Alexa flash briefing was found for: %s' + err = "No configured Alexa flash briefing was found for: %s" _LOGGER.error(err, briefing_id) return b'', 404 diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 8283b563591..b6d406bd550 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -3,30 +3,31 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ - """ import asyncio import enum import logging -from homeassistant.exceptions import HomeAssistantError -from homeassistant.core import callback -from homeassistant.helpers import intent from homeassistant.components import http +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import intent from homeassistant.util.decorator import Registry from .const import DOMAIN, SYN_RESOLUTION_MATCH -INTENTS_API_ENDPOINT = '/api/alexa' -HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) +HANDLERS = Registry() + +INTENTS_API_ENDPOINT = '/api/alexa' + class SpeechType(enum.Enum): """The Alexa speech types.""" - plaintext = "PlainText" - ssml = "SSML" + plaintext = 'PlainText' + ssml = 'SSML' SPEECH_MAPPINGS = { @@ -38,8 +39,8 @@ SPEECH_MAPPINGS = { class CardType(enum.Enum): """The Alexa card types.""" - simple = "Simple" - link_account = "LinkAccount" + simple = 'Simple' + link_account = 'LinkAccount' @callback @@ -64,7 +65,7 @@ class AlexaIntentsView(http.HomeAssistantView): hass = request.app['hass'] message = yield from request.json() - _LOGGER.debug('Received Alexa request: %s', message) + _LOGGER.debug("Received Alexa request: %s", message) try: response = yield from async_handle_message(hass, message) @@ -81,7 +82,7 @@ class AlexaIntentsView(http.HomeAssistantView): "This intent is not yet configured within Home Assistant.")) except intent.InvalidSlotInfo as err: - _LOGGER.error('Received invalid slot data from Alexa: %s', err) + _LOGGER.error("Received invalid slot data from Alexa: %s", err) return self.json(intent_error_response( hass, message, "Invalid slot information received for this intent.")) @@ -109,6 +110,7 @@ def async_handle_message(hass, message): - intent.UnknownIntent - intent.InvalidSlotInfo - intent.IntentError + """ req = message.get('request') req_type = req['type'] @@ -138,6 +140,7 @@ def async_handle_intent(hass, message): - intent.UnknownIntent - intent.InvalidSlotInfo - intent.IntentError + """ req = message.get('request') alexa_intent_info = req.get('intent') diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 3c14826037c..2fae0b323a0 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -2,75 +2,374 @@ import asyncio import logging import math +from datetime import datetime from uuid import uuid4 +from homeassistant.components import ( + alert, automation, cover, fan, group, input_boolean, light, lock, + media_player, scene, script, switch, http, sensor) import homeassistant.core as ha +import homeassistant.util.color as color_util +from homeassistant.util.decorator import Registry from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET) -from homeassistant.components import ( - alert, automation, cover, fan, group, input_boolean, light, lock, - media_player, scene, script, switch) -import homeassistant.util.color as color_util -from homeassistant.util.decorator import Registry + SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, + CONF_UNIT_OF_MEASUREMENT) +from .const import CONF_FILTER, CONF_ENTITY_CONFIG -HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) API_DIRECTIVE = 'directive' API_ENDPOINT = 'endpoint' API_EVENT = 'event' +API_CONTEXT = 'context' API_HEADER = 'header' API_PAYLOAD = 'payload' +API_TEMP_UNITS = { + TEMP_FAHRENHEIT: 'FAHRENHEIT', + TEMP_CELSIUS: 'CELSIUS', +} + +SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' + CONF_DESCRIPTION = 'description' CONF_DISPLAY_CATEGORIES = 'display_categories' -CONF_NAME = 'name' + +HANDLERS = Registry() -MAPPING_COMPONENT = { - alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], - automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], - cover.DOMAIN: [ - 'DOOR', ('Alexa.PowerController',), { - cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController', - } - ], - fan.DOMAIN: [ - 'OTHER', ('Alexa.PowerController',), { - fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController', - } - ], - group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], - input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], - light.DOMAIN: [ - 'LIGHT', ('Alexa.PowerController',), { - light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController', - light.SUPPORT_RGB_COLOR: 'Alexa.ColorController', - light.SUPPORT_XY_COLOR: 'Alexa.ColorController', - light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController', - } - ], - lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None], - media_player.DOMAIN: [ - 'TV', ('Alexa.PowerController',), { - media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker', - media_player.SUPPORT_PLAY: 'Alexa.PlaybackController', - media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController', - media_player.SUPPORT_STOP: 'Alexa.PlaybackController', - media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController', - media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController', - } - ], - scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None], - script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], - switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], +class _DisplayCategory(object): + """Possible display categories for Discovery response. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories + """ + + # Describes a combination of devices set to a specific state, when the + # state change must occur in a specific order. For example, a "watch + # Neflix" scene might require the: 1. TV to be powered on & 2. Input set to + # HDMI1. Applies to Scenes + ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER" + + # Indicates media devices with video or photo capabilities. + CAMERA = "CAMERA" + + # Indicates a door. + DOOR = "DOOR" + + # Indicates light sources or fixtures. + LIGHT = "LIGHT" + + # An endpoint that cannot be described in on of the other categories. + OTHER = "OTHER" + + # Describes a combination of devices set to a specific state, when the + # order of the state change is not important. For example a bedtime scene + # might include turning off lights and lowering the thermostat, but the + # order is unimportant. Applies to Scenes + SCENE_TRIGGER = "SCENE_TRIGGER" + + # Indicates an endpoint that locks. + SMARTLOCK = "SMARTLOCK" + + # Indicates modules that are plugged into an existing electrical outlet. + # Can control a variety of devices. + SMARTPLUG = "SMARTPLUG" + + # Indicates the endpoint is a speaker or speaker system. + SPEAKER = "SPEAKER" + + # Indicates in-wall switches wired to the electrical system. Can control a + # variety of devices. + SWITCH = "SWITCH" + + # Indicates endpoints that report the temperature only. + TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR" + + # Indicates endpoints that control temperature, stand-alone air + # conditioners, or heaters with direct temperature control. + THERMOSTAT = "THERMOSTAT" + + # Indicates the endpoint is a television. + # pylint: disable=invalid-name + TV = "TV" + + +def _capability(interface, + version=3, + supports_deactivation=None, + retrievable=None, + properties_supported=None, + cap_type='AlexaInterface'): + """Return a Smart Home API capability object. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#capability-object + + There are some additional fields allowed but not implemented here since + we've no use case for them yet: + + - proactively_reported + + `supports_deactivation` applies only to scenes. + """ + result = { + 'type': cap_type, + 'interface': interface, + 'version': version, + } + + if supports_deactivation is not None: + result['supportsDeactivation'] = supports_deactivation + + if retrievable is not None: + result['retrievable'] = retrievable + + if properties_supported is not None: + result['properties'] = {'supported': properties_supported} + + return result + + +class _EntityCapabilities(object): + def __init__(self, config, entity): + self.config = config + self.entity = entity + + def display_categories(self): + """Return a list of display categories.""" + entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) + if CONF_DISPLAY_CATEGORIES in entity_conf: + return [entity_conf[CONF_DISPLAY_CATEGORIES]] + return self.default_display_categories() + + def default_display_categories(self): + """Return a list of default display categories. + + This can be overridden by the user in the Home Assistant configuration. + + See also _DisplayCategory. + """ + raise NotImplementedError + + def capabilities(self): + """Return a list of supported capabilities. + + If the returned list is empty, the entity will not be discovered. + + You might find _capability() useful. + """ + raise NotImplementedError + + +class _GenericCapabilities(_EntityCapabilities): + """A generic, on/off device. + + The choice of last resort. + """ + + def default_display_categories(self): + return [_DisplayCategory.OTHER] + + def capabilities(self): + return [_capability('Alexa.PowerController')] + + +class _SwitchCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.SWITCH] + + def capabilities(self): + return [_capability('Alexa.PowerController')] + + +class _CoverCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.DOOR] + + def capabilities(self): + capabilities = [_capability('Alexa.PowerController')] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & cover.SUPPORT_SET_POSITION: + capabilities.append(_capability('Alexa.PercentageController')) + return capabilities + + +class _LightCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.LIGHT] + + def capabilities(self): + capabilities = [_capability('Alexa.PowerController')] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & light.SUPPORT_BRIGHTNESS: + capabilities.append(_capability('Alexa.BrightnessController')) + if supported & light.SUPPORT_RGB_COLOR: + capabilities.append(_capability('Alexa.ColorController')) + if supported & light.SUPPORT_XY_COLOR: + capabilities.append(_capability('Alexa.ColorController')) + if supported & light.SUPPORT_COLOR_TEMP: + capabilities.append( + _capability('Alexa.ColorTemperatureController')) + return capabilities + + +class _FanCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.OTHER] + + def capabilities(self): + capabilities = [_capability('Alexa.PowerController')] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & fan.SUPPORT_SET_SPEED: + capabilities.append(_capability('Alexa.PercentageController')) + return capabilities + + +class _LockCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.SMARTLOCK] + + def capabilities(self): + return [_capability('Alexa.LockController')] + + +class _MediaPlayerCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.TV] + + def capabilities(self): + capabilities = [_capability('Alexa.PowerController')] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & media_player.SUPPORT_VOLUME_SET: + capabilities.append(_capability('Alexa.Speaker')) + + playback_features = (media_player.SUPPORT_PLAY | + media_player.SUPPORT_PAUSE | + media_player.SUPPORT_STOP | + media_player.SUPPORT_NEXT_TRACK | + media_player.SUPPORT_PREVIOUS_TRACK) + if supported & playback_features: + capabilities.append(_capability('Alexa.PlaybackController')) + + return capabilities + + +class _SceneCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.SCENE_TRIGGER] + + def capabilities(self): + return [_capability('Alexa.SceneController')] + + +class _ScriptCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.ACTIVITY_TRIGGER] + + def capabilities(self): + can_cancel = bool(self.entity.attributes.get('can_cancel')) + return [_capability('Alexa.SceneController', + supports_deactivation=can_cancel)] + + +class _GroupCapabilities(_EntityCapabilities): + def default_display_categories(self): + return [_DisplayCategory.SCENE_TRIGGER] + + def capabilities(self): + return [_capability('Alexa.SceneController', + supports_deactivation=True)] + + +class _SensorCapabilities(_EntityCapabilities): + def default_display_categories(self): + # although there are other kinds of sensors, all but temperature + # sensors are currently ignored. + return [_DisplayCategory.TEMPERATURE_SENSOR] + + def capabilities(self): + capabilities = [] + + attrs = self.entity.attributes + if attrs.get(CONF_UNIT_OF_MEASUREMENT) in ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + ): + capabilities.append(_capability( + 'Alexa.TemperatureSensor', + retrievable=True, + properties_supported=[{'name': 'temperature'}])) + + return capabilities + + +class _UnknownEntityDomainError(Exception): + pass + + +def _capabilities_for_entity(config, entity): + """Return an _EntityCapabilities appropriate for given entity. + + raises _UnknownEntityDomainError if the given domain is unsupported. + """ + if entity.domain not in _CAPABILITIES_FOR_DOMAIN: + raise _UnknownEntityDomainError() + return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity) + + +_CAPABILITIES_FOR_DOMAIN = { + alert.DOMAIN: _GenericCapabilities, + automation.DOMAIN: _GenericCapabilities, + cover.DOMAIN: _CoverCapabilities, + fan.DOMAIN: _FanCapabilities, + group.DOMAIN: _GroupCapabilities, + input_boolean.DOMAIN: _GenericCapabilities, + light.DOMAIN: _LightCapabilities, + lock.DOMAIN: _LockCapabilities, + media_player.DOMAIN: _MediaPlayerCapabilities, + scene.DOMAIN: _SceneCapabilities, + script.DOMAIN: _ScriptCapabilities, + switch.DOMAIN: _SwitchCapabilities, + sensor.DOMAIN: _SensorCapabilities, } +class _Cause(object): + """Possible causes for property changes. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object + """ + + # Indicates that the event was caused by a customer interaction with an + # application. For example, a customer switches on a light, or locks a door + # using the Alexa app or an app provided by a device vendor. + APP_INTERACTION = 'APP_INTERACTION' + + # Indicates that the event was caused by a physical interaction with an + # endpoint. For example manually switching on a light or manually locking a + # door lock + PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION' + + # Indicates that the event was caused by the periodic poll of an appliance, + # which found a change in value. For example, you might poll a temperature + # sensor every hour, and send the updated temperature to Alexa. + PERIODIC_POLL = 'PERIODIC_POLL' + + # Indicates that the event was caused by the application of a device rule. + # For example, a customer configures a rule to switch on a light if a + # motion sensor detects motion. In this case, Alexa receives an event from + # the motion sensor, and another event from the light to indicate that its + # state change was caused by the rule. + RULE_TRIGGER = 'RULE_TRIGGER' + + # Indicates that the event was caused by a voice interaction with Alexa. + # For example a user speaking to their Echo device. + VOICE_INTERACTION = 'VOICE_INTERACTION' + + class Config: """Hold the configuration for Alexa.""" @@ -80,6 +379,52 @@ class Config: self.entity_config = entity_config or {} +@ha.callback +def async_setup(hass, config): + """Activate Smart Home functionality of Alexa component. + + This is optional, triggered by having a `smart_home:` sub-section in the + alexa configuration. + + Even if that's disabled, the functionality in this module may still be used + by the cloud component which will call async_handle_message directly. + """ + smart_home_config = Config( + should_expose=config[CONF_FILTER], + entity_config=config.get(CONF_ENTITY_CONFIG), + ) + hass.http.register_view(SmartHomeView(smart_home_config)) + + +class SmartHomeView(http.HomeAssistantView): + """Expose Smart Home v3 payload interface via HTTP POST.""" + + url = SMART_HOME_HTTP_ENDPOINT + name = 'api:alexa:smart_home' + + def __init__(self, smart_home_config): + """Initialize.""" + self.smart_home_config = smart_home_config + + @asyncio.coroutine + def post(self, request): + """Handle Alexa Smart Home requests. + + The Smart Home API requires the endpoint to be implemented in AWS + Lambda, which will need to forward the requests to here and pass back + the response. + """ + hass = request.app['hass'] + message = yield from request.json() + + _LOGGER.debug("Received Alexa Smart Home request: %s", message) + + response = yield from async_handle_message( + hass, self.smart_home_config, message) + _LOGGER.debug("Sending Alexa Smart Home response: %s", response) + return b'' if response is None else self.json(response) + + @asyncio.coroutine def async_handle_message(hass, config, message): """Handle incoming API messages.""" @@ -100,7 +445,11 @@ def async_handle_message(hass, config, message): return (yield from funct_ref(hass, config, message)) -def api_message(request, name='Response', namespace='Alexa', payload=None): +def api_message(request, + name='Response', + namespace='Alexa', + payload=None, + context=None): """Create a API formatted response message. Async friendly. @@ -128,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None): if API_ENDPOINT in request: response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy() + if context is not None: + response[API_CONTEXT] = context + return response @@ -159,9 +511,9 @@ def async_api_discovery(hass, config, request): entity.entity_id) continue - class_data = MAPPING_COMPONENT.get(entity.domain) - - if not class_data: + try: + entity_capabilities = _capabilities_for_entity(config, entity) + except _UnknownEntityDomainError: continue entity_conf = config.entity_config.get(entity.entity_id, {}) @@ -174,40 +526,21 @@ def async_api_discovery(hass, config, request): scene_fmt = '{} (Scene connected via Home Assistant)' description = scene_fmt.format(description) - display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES, - class_data[0]) - endpoint = { - 'displayCategories': [display_categories], + 'displayCategories': entity_capabilities.display_categories(), 'additionalApplianceDetails': {}, 'endpointId': entity.entity_id.replace('.', '#'), 'friendlyName': friendly_name, 'description': description, 'manufacturerName': 'Home Assistant', } - actions = set() - # static actions - if class_data[1]: - actions |= set(class_data[1]) - - # dynamic actions - if class_data[2]: - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - for feature, action_name in class_data[2].items(): - if feature & supported > 0: - actions.add(action_name) - - # Write action into capabilities - capabilities = [] - for action in actions: - capabilities.append({ - 'type': 'AlexaInterface', - 'interface': action, - 'version': 3, - }) - - endpoint['capabilities'] = capabilities + alexa_capabilities = entity_capabilities.capabilities() + if not alexa_capabilities: + _LOGGER.debug("Not exposing %s because it has no capabilities", + entity.entity_id) + continue + endpoint['capabilities'] = alexa_capabilities discovery_endpoints.append(endpoint) return api_message( @@ -216,7 +549,7 @@ def async_api_discovery(hass, config, request): def extract_entity(funct): - """Decorator for extract entity object from request.""" + """Decorate for extract entity object from request.""" @asyncio.coroutine def async_api_entity_wrapper(hass, config, request): """Process a turn on request.""" @@ -240,8 +573,6 @@ def extract_entity(funct): def async_api_turn_on(hass, config, request, entity): """Process a turn on request.""" domain = entity.domain - if entity.domain == group.DOMAIN: - domain = ha.DOMAIN service = SERVICE_TURN_ON if entity.domain == cover.DOMAIN: @@ -379,7 +710,7 @@ def async_api_decrease_color_temp(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_increase_color_temp(hass, config, request, entity): - """Process a increase color temperature request.""" + """Process an increase color temperature request.""" current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) @@ -396,12 +727,54 @@ def async_api_increase_color_temp(hass, config, request, entity): @extract_entity @asyncio.coroutine def async_api_activate(hass, config, request, entity): - """Process a activate request.""" - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + """Process an activate request.""" + if entity.domain == group.DOMAIN: + domain = ha.DOMAIN + else: + domain = entity.domain + + yield from hass.services.async_call(domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id }, blocking=False) - return api_message(request) + payload = { + 'cause': {'type': _Cause.VOICE_INTERACTION}, + 'timestamp': '%sZ' % (datetime.utcnow().isoformat(),) + } + + return api_message( + request, + name='ActivationStarted', + namespace='Alexa.SceneController', + payload=payload, + ) + + +@HANDLERS.register(('Alexa.SceneController', 'Deactivate')) +@extract_entity +@asyncio.coroutine +def async_api_deactivate(hass, config, request, entity): + """Process a deactivate request.""" + if entity.domain == group.DOMAIN: + domain = ha.DOMAIN + else: + domain = entity.domain + + yield from hass.services.async_call(domain, SERVICE_TURN_OFF, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=False) + + payload = { + 'cause': {'type': _Cause.VOICE_INTERACTION}, + 'timestamp': '%sZ' % (datetime.utcnow().isoformat(),) + } + + return api_message( + request, + name='DeactivationStarted', + namespace='Alexa.SceneController', + payload=payload, + ) @HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) @@ -653,3 +1026,25 @@ def async_api_previous(hass, config, request, entity): data, blocking=False) return api_message(request) + + +@HANDLERS.register(('Alexa', 'ReportState')) +@extract_entity +@asyncio.coroutine +def async_api_reportstate(hass, config, request, entity): + """Process a ReportState request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp_property = { + 'namespace': 'Alexa.TemperatureSensor', + 'name': 'temperature', + 'value': { + 'value': float(entity.state), + 'scale': API_TEMP_UNITS[unit], + }, + } + + return api_message( + request, + name='StateReport', + context={'properties': [temp_property]} + ) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index beacb3840ef..230b0ea8a1b 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -7,13 +7,14 @@ https://home-assistant.io/components/apple_tv/ import asyncio import logging +from typing import Sequence, TypeVar, Union + import voluptuous as vol -from typing import Union, TypeVar, Sequence -from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import discovery from homeassistant.components.discovery import SERVICE_APPLE_TV +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyatv==0.3.9'] @@ -59,9 +60,9 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(ensure_list, [vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_LOGIN_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CREDENTIALS, default=None): cv.string, - vol.Optional(CONF_START_OFF, default=False): cv.boolean + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_START_OFF, default=False): cv.boolean, })]) }, extra=vol.ALLOW_EXTRA) @@ -140,7 +141,7 @@ def async_setup(hass, config): @asyncio.coroutine def async_service_handler(service): - """Handler for service calls.""" + """Handle service calls.""" entity_ids = service.data.get(ATTR_ENTITY_ID) if service.service == SERVICE_SCAN: @@ -167,7 +168,7 @@ def async_setup(hass, config): @asyncio.coroutine def atv_discovered(service, info): - """Setup an Apple TV that was auto discovered.""" + """Set up an Apple TV that was auto discovered.""" yield from _setup_atv(hass, { CONF_NAME: info['name'], CONF_HOST: info['host'], @@ -194,7 +195,7 @@ def async_setup(hass, config): @asyncio.coroutine def _setup_atv(hass, atv_config): - """Setup an Apple TV.""" + """Set up an Apple TV.""" import pyatv name = atv_config.get(CONF_NAME) host = atv_config.get(CONF_HOST) @@ -245,7 +246,7 @@ class AppleTVPowerManager: @property def turned_on(self): - """If device is on or off.""" + """Return true if device is on or off.""" return self._is_on def set_power_on(self, value): diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index c1dafb87a6d..0b5e7c1e1d7 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -1,34 +1,34 @@ -"""Support for Asterisk Voicemail interface.""" +""" +Support for Asterisk Voicemail interface. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/asterisk_mbox/ +""" import logging import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import (CONF_HOST, - CONF_PORT, CONF_PASSWORD) - +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback -from homeassistant.helpers.dispatcher import (async_dispatcher_connect, - async_dispatcher_send) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) REQUIREMENTS = ['asterisk_mbox==0.4.0'] -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' -SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' +_LOGGER = logging.getLogger(__name__) DOMAIN = 'asterisk_mbox' -_LOGGER = logging.getLogger(__name__) - +SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' +SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): int, vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_PORT): int, }), }, extra=vol.ALLOW_EXTRA) @@ -43,7 +43,7 @@ def setup(hass, config): hass.data[DOMAIN] = AsteriskData(hass, host, port, password) - discovery.load_platform(hass, "mailbox", DOMAIN, {}, config) + discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config) return True @@ -68,15 +68,14 @@ class AsteriskData(object): from asterisk_mbox.commands import CMD_MESSAGE_LIST if command == CMD_MESSAGE_LIST: - _LOGGER.info("AsteriskVM sent updated message list") - self.messages = sorted(msg, - key=lambda item: item['info']['origtime'], - reverse=True) - async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, - self.messages) + _LOGGER.debug("AsteriskVM sent updated message list") + self.messages = sorted( + msg, key=lambda item: item['info']['origtime'], reverse=True) + async_dispatcher_send( + self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) @callback def _request_messages(self): """Handle changes to the mailbox.""" - _LOGGER.info("Requesting message list") + _LOGGER.debug("Requesting message list") self.client.messages() diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index bc3c17e41da..8c490754f40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -338,10 +338,9 @@ class AutomationEntity(ToggleEntity): yield from self.async_update_ha_state() @asyncio.coroutine - def async_remove(self): - """Remove automation from HASS.""" + def async_will_remove_from_hass(self): + """Remove listeners when removing automation from HASS.""" yield from self.async_turn_off() - yield from super().async_remove() @asyncio.coroutine def async_enable(self): diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index 23dbe052d1c..fab7d98ed98 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -4,24 +4,21 @@ Support for Axis devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/axis/ """ - import logging import voluptuous as vol from homeassistant.components.discovery import SERVICE_AXIS -from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED, - CONF_EVENT, CONF_HOST, CONF_INCLUDE, - CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_TRIGGER_TIME, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.util.json import load_json, save_json - REQUIREMENTS = ['axis==14'] _LOGGER = logging.getLogger(__name__) @@ -81,10 +78,10 @@ def request_configuration(hass, config, name, host, serialnumber): configurator = hass.components.configurator def configuration_callback(callback_data): - """Called when config is submitted.""" + """Call when configuration is submitted.""" if CONF_INCLUDE not in callback_data: - configurator.notify_errors(request_id, - "Functionality mandatory.") + configurator.notify_errors( + request_id, "Functionality mandatory.") return False callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split() @@ -96,18 +93,20 @@ def request_configuration(hass, config, name, host, serialnumber): try: device_config = DEVICE_SCHEMA(callback_data) except vol.Invalid: - configurator.notify_errors(request_id, - "Bad input, please check spelling.") + configurator.notify_errors( + request_id, "Bad input, please check spelling.") return False if setup_device(hass, config, device_config): + del device_config['events'] + del device_config['signal'] config_file = load_json(hass.config.path(CONFIG_FILE)) config_file[serialnumber] = dict(device_config) save_json(hass.config.path(CONFIG_FILE), config_file) configurator.request_done(request_id) else: - configurator.notify_errors(request_id, - "Failed to register, please try again.") + configurator.notify_errors( + request_id, "Failed to register, please try again.") return False title = '{} ({})'.format(name, host) @@ -145,7 +144,7 @@ def request_configuration(hass, config, name, host, serialnumber): def setup(hass, config): - """Common setup for Axis devices.""" + """Set up for Axis devices.""" def _shutdown(call): # pylint: disable=unused-argument """Stop the event stream on shutdown.""" for serialnumber, device in AXIS_DEVICES.items(): @@ -155,7 +154,7 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) def axis_device_discovered(service, discovery_info): - """Called when axis devices has been found.""" + """Call when axis devices has been found.""" host = discovery_info[CONF_HOST] name = discovery_info['hostname'] serialnumber = discovery_info['properties']['macaddress'] @@ -171,8 +170,8 @@ def setup(hass, config): _LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err) return False if not setup_device(hass, config, device_config): - _LOGGER.error("Couldn\'t set up %s", - device_config[CONF_NAME]) + _LOGGER.error( + "Couldn't set up %s", device_config[CONF_NAME]) else: # New device, create configuration request for UI request_configuration(hass, config, name, host, serialnumber) @@ -191,7 +190,7 @@ def setup(hass, config): if CONF_NAME not in device_config: device_config[CONF_NAME] = device if not setup_device(hass, config, device_config): - _LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME]) + _LOGGER.error("Couldn't set up %s", device_config[CONF_NAME]) def vapix_service(call): """Service to send a message.""" @@ -203,23 +202,21 @@ def setup(hass, config): call.data[SERVICE_PARAM]) hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response) return True - _LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME]) + _LOGGER.info("Couldn't find device %s", call.data[CONF_NAME]) return False # Register service with Home Assistant. - hass.services.register(DOMAIN, - SERVICE_VAPIX_CALL, - vapix_service, - schema=SERVICE_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA) return True def setup_device(hass, config, device_config): - """Set up device.""" + """Set up an Axis device.""" from axis import AxisDevice def signal_callback(action, event): - """Callback to configure events when initialized on event stream.""" + """Call to configure events when initialized on event stream.""" if action == 'add': event_config = { CONF_EVENT: event, @@ -228,11 +225,8 @@ def setup_device(hass, config, device_config): CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME] } component = event.event_platform - discovery.load_platform(hass, - component, - DOMAIN, - event_config, - config) + discovery.load_platform( + hass, component, DOMAIN, event_config, config) event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], EVENT_TYPES)) @@ -243,7 +237,7 @@ def setup_device(hass, config, device_config): if device.serial_number is None: # If there is no serial number a connection could not be made - _LOGGER.error("Couldn\'t connect to %s", device_config[CONF_HOST]) + _LOGGER.error("Couldn't connect to %s", device_config[CONF_HOST]) return False for component in device_config[CONF_INCLUDE]: @@ -255,11 +249,8 @@ def setup_device(hass, config, device_config): CONF_USERNAME: device_config[CONF_USERNAME], CONF_PASSWORD: device_config[CONF_PASSWORD] } - discovery.load_platform(hass, - component, - DOMAIN, - camera_config, - config) + discovery.load_platform( + hass, component, DOMAIN, camera_config, config) AXIS_DEVICES[device.serial_number] = device if event_types: @@ -273,9 +264,9 @@ class AxisDeviceEvent(Entity): def __init__(self, event_config): """Initialize the event.""" self.axis_event = event_config[CONF_EVENT] - self._name = '{}_{}_{}'.format(event_config[CONF_NAME], - self.axis_event.event_type, - self.axis_event.id) + self._name = '{}_{}_{}'.format( + event_config[CONF_NAME], self.axis_event.event_type, + self.axis_event.id) self.location = event_config[ATTR_LOCATION] self.axis_event.callback = self._update_callback @@ -296,7 +287,7 @@ class AxisDeviceEvent(Entity): @property def should_poll(self): - """No polling needed.""" + """Return the polling state. No polling needed.""" return False @property diff --git a/homeassistant/components/binary_sensor/ads.py b/homeassistant/components/binary_sensor/ads.py index e6b86ed97e6..b7f0ebcc9d3 100644 --- a/homeassistant/components/binary_sensor/ads.py +++ b/homeassistant/components/binary_sensor/ads.py @@ -3,23 +3,22 @@ Support for ADS binary sensors. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/binary_sensor.ads/ - """ import asyncio import logging -import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, \ - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA -from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS -import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['ads'] DEFAULT_NAME = 'ADS binary sensor' - +DEPENDENCIES = ['ads'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ADS_VAR): cv.string, @@ -44,7 +43,7 @@ class AdsBinarySensor(BinarySensorDevice): """Representation of ADS binary sensors.""" def __init__(self, ads_hub, name, ads_var, device_class): - """Initialize AdsBinarySensor entity.""" + """Initialize ADS binary sensor.""" self._name = name self._state = False self._device_class = device_class or 'moving' @@ -56,15 +55,13 @@ class AdsBinarySensor(BinarySensorDevice): """Register device notification.""" def update(name, value): """Handle device notifications.""" - _LOGGER.debug('Variable %s changed its value to %d', - name, value) + _LOGGER.debug('Variable %s changed its value to %d', name, value) self._state = value self.schedule_update_ha_state() self.hass.async_add_job( self._ads_hub.add_device_notification, - self.ads_var, self._ads_hub.PLCTYPE_BOOL, update - ) + self.ads_var, self._ads_hub.PLCTYPE_BOOL, update) @property def name(self): diff --git a/homeassistant/components/binary_sensor/axis.py b/homeassistant/components/binary_sensor/axis.py index a6e80dbf97f..84137d95b06 100644 --- a/homeassistant/components/binary_sensor/axis.py +++ b/homeassistant/components/binary_sensor/axis.py @@ -4,13 +4,12 @@ Support for Axis binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.axis/ """ - -import logging from datetime import timedelta +import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.axis import (AxisDeviceEvent) -from homeassistant.const import (CONF_TRIGGER_TIME) +from homeassistant.components.axis import AxisDeviceEvent +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_TRIGGER_TIME from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util.dt import utcnow @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Axis device event.""" + """Set up the Axis binary devices.""" add_devices([AxisBinarySensor(hass, discovery_info)], True) @@ -28,7 +27,7 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice): """Representation of a binary Axis event.""" def __init__(self, hass, event_config): - """Initialize the binary sensor.""" + """Initialize the Axis binary sensor.""" self.hass = hass self._state = False self._delay = event_config[CONF_TRIGGER_TIME] @@ -56,7 +55,7 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice): # Set timer to wait until updating the state def _delay_update(now): """Timer callback for sensor update.""" - _LOGGER.debug("%s Called delayed (%s sec) update.", + _LOGGER.debug("%s called delayed (%s sec) update", self._name, self._delay) self.schedule_update_ha_state() self._timer = None diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index c8442491b29..3bac561700a 100644 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] try: - _LOGGER.debug("Initializing Client") + _LOGGER.debug("Initializing client") client = concord232_client.Client('http://{}:{}'.format(host, port)) client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 97f78ff21d0..3c02dfb3508 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -4,7 +4,6 @@ Support for deCONZ binary sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.deconz/ """ - import asyncio from homeassistant.components.binary_sensor import BinarySensorDevice @@ -17,7 +16,7 @@ DEPENDENCIES = ['deconz'] @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup binary sensor for deCONZ component.""" + """Set up the deCONZ binary sensor.""" if discovery_info is None: return @@ -25,8 +24,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = hass.data[DECONZ_DATA].sensors entities = [] - for sensor in sensors.values(): - if sensor.type in DECONZ_BINARY_SENSOR: + for key in sorted(sensors.keys(), key=int): + sensor = sensors[key] + if sensor and sensor.type in DECONZ_BINARY_SENSOR: entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) @@ -35,7 +35,7 @@ class DeconzBinarySensor(BinarySensorDevice): """Representation of a binary sensor.""" def __init__(self, sensor): - """Setup sensor and add update callback to get data from websocket.""" + """Set up sensor and add update callback to get data from websocket.""" self._sensor = sensor @asyncio.coroutine @@ -67,7 +67,7 @@ class DeconzBinarySensor(BinarySensorDevice): @property def device_class(self): - """Class of the sensor.""" + """Return the class of the sensor.""" return self._sensor.sensor_class @property diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py index 51fffae5cc0..170f1818a0e 100644 --- a/homeassistant/components/binary_sensor/flic.py +++ b/homeassistant/components/binary_sensor/flic.py @@ -238,6 +238,5 @@ class FlicButton(BinarySensorDevice): import pyflic if connection_status == pyflic.ConnectionStatus.Disconnected: - _LOGGER.info("Button (%s) disconnected. Reason: %s", - self.address, disconnect_reason) - self.remove() + _LOGGER.warning("Button (%s) disconnected. Reason: %s", + self.address, disconnect_reason) diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py index b62c003c4fd..6223ebe50a1 100644 --- a/homeassistant/components/binary_sensor/hive.py +++ b/homeassistant/components/binary_sensor/hive.py @@ -1,63 +1,63 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hive/ -""" -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.hive import DATA_HIVE - -DEPENDENCIES = ['hive'] - -DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion', - 'contactsensor': 'opening'} - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Hive sensor devices.""" - if discovery_info is None: - return - session = hass.data.get(DATA_HIVE) - - add_devices([HiveBinarySensorEntity(session, discovery_info)]) - - -class HiveBinarySensorEntity(BinarySensorDevice): - """Representation of a Hive binary sensor.""" - - def __init__(self, hivesession, hivedevice): - """Initialize the hive sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) - - self.session.entities.append(self) - - def handle_update(self, updatesource): - """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: - self.schedule_update_ha_state() - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type) - - @property - def name(self): - """Return the name of the binary sensor.""" - return self.node_name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.session.sensor.get_state(self.node_id, - self.node_device_type) - - def update(self): - """Update all Node data frome Hive.""" - self.session.core.update_data(self.node_id) +""" +Support for the Hive devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hive/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.hive import DATA_HIVE + +DEPENDENCIES = ['hive'] + +DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion', + 'contactsensor': 'opening'} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Hive sensor devices.""" + if discovery_info is None: + return + session = hass.data.get(DATA_HIVE) + + add_devices([HiveBinarySensorEntity(session, discovery_info)]) + + +class HiveBinarySensorEntity(BinarySensorDevice): + """Representation of a Hive binary sensor.""" + + def __init__(self, hivesession, hivedevice): + """Initialize the hive sensor.""" + self.node_id = hivedevice["Hive_NodeID"] + self.node_name = hivedevice["Hive_NodeName"] + self.device_type = hivedevice["HA_DeviceType"] + self.node_device_type = hivedevice["Hive_DeviceType"] + self.session = hivesession + self.data_updatesource = '{}.{}'.format(self.device_type, + self.node_id) + + self.session.entities.append(self) + + def handle_update(self, updatesource): + """Handle the new update request.""" + if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + self.schedule_update_ha_state() + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type) + + @property + def name(self): + """Return the name of the binary sensor.""" + return self.node_name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.session.sensor.get_state(self.node_id, + self.node_device_type) + + def update(self): + """Update all Node data frome Hive.""" + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py new file mode 100644 index 00000000000..14e45f88cf1 --- /dev/null +++ b/homeassistant/components/binary_sensor/ihc.py @@ -0,0 +1,95 @@ +"""IHC binary sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.ihc/ +""" +from xml.etree.ElementTree import Element + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) +from homeassistant.components.ihc import ( + validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO) +from homeassistant.components.ihc.const import CONF_INVERTING +from homeassistant.components.ihc.ihcdevice import IHCDevice +from homeassistant.const import ( + CONF_NAME, CONF_TYPE, CONF_ID, CONF_BINARY_SENSORS) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ihc'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_BINARY_SENSORS, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_ID): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE, default=None): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_INVERTING, default=False): cv.boolean, + }, validate_name) + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the IHC binary sensor platform.""" + ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER] + info = hass.data[IHC_DATA][IHC_INFO] + devices = [] + if discovery_info: + for name, device in discovery_info.items(): + ihc_id = device['ihc_id'] + product_cfg = device['product_cfg'] + product = device['product'] + sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info, + product_cfg[CONF_TYPE], + product_cfg[CONF_INVERTING], + product) + devices.append(sensor) + else: + binary_sensors = config[CONF_BINARY_SENSORS] + for sensor_cfg in binary_sensors: + ihc_id = sensor_cfg[CONF_ID] + name = sensor_cfg[CONF_NAME] + sensor_type = sensor_cfg[CONF_TYPE] + inverting = sensor_cfg[CONF_INVERTING] + sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info, + sensor_type, inverting) + devices.append(sensor) + + add_devices(devices) + + +class IHCBinarySensor(IHCDevice, BinarySensorDevice): + """IHC Binary Sensor. + + The associated IHC resource can be any in or output from a IHC product + or function block, but it must be a boolean ON/OFF resources. + """ + + def __init__(self, ihc_controller, name, ihc_id: int, info: bool, + sensor_type: str, inverting: bool, product: Element=None): + """Initialize the IHC binary sensor.""" + super().__init__(ihc_controller, name, ihc_id, info, product) + self._state = None + self._sensor_type = sensor_type + self.inverting = inverting + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._sensor_type + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._state + + def on_ihc_change(self, ihc_id, value): + """IHC resource has changed.""" + if self.inverting: + self._state = not value + else: + self._state = value + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index 9e5ddf5cac4..c01654a3663 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -5,12 +5,13 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.knx/ """ import asyncio + import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES, \ - KNXAutomation -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, \ - BinarySensorDevice +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.knx import ( + ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -53,20 +54,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up binary sensor(s) for KNX platform.""" - if DATA_KNX not in hass.data \ - or not hass.data[DATA_KNX].initialized: - return False + if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized: + return if discovery_info is not None: async_add_devices_discovery(hass, discovery_info, async_add_devices) else: async_add_devices_config(hass, config, async_add_devices) - return True - @callback def async_add_devices_discovery(hass, discovery_info, async_add_devices): @@ -80,7 +77,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up binary senor for KNX platform configured within plattform.""" + """Set up binary senor for KNX platform configured within platform.""" name = config.get(CONF_NAME) import xknx binary_sensor = xknx.devices.BinarySensor( @@ -108,7 +105,7 @@ class KNXBinarySensor(BinarySensorDevice): """Representation of a KNX binary sensor.""" def __init__(self, hass, device): - """Initialization of KNXBinarySensor.""" + """Initialize of KNX binary sensor.""" self.device = device self.hass = hass self.async_register_callbacks() @@ -119,7 +116,7 @@ class KNXBinarySensor(BinarySensorDevice): """Register callbacks to update hass after device was changed.""" @asyncio.coroutine def after_update_callback(device): - """Callback after device was updated.""" + """Call after device was updated.""" # pylint: disable=unused-argument yield from self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py index cf2be6baed5..1043004243a 100644 --- a/homeassistant/components/binary_sensor/maxcube.py +++ b/homeassistant/components/binary_sensor/maxcube.py @@ -36,7 +36,7 @@ class MaxCubeShutter(BinarySensorDevice): def __init__(self, hass, name, rf_address): """Initialize MAX! Cube BinarySensorDevice.""" self._name = name - self._sensor_type = 'opening' + self._sensor_type = 'window' self._rf_address = rf_address self._cubehandle = hass.data[MAXCUBE_HANDLE] self._state = STATE_UNKNOWN diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py new file mode 100644 index 00000000000..a89395ed86f --- /dev/null +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -0,0 +1,85 @@ +"""Support for MyChevy sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mychevy/ +""" + +import asyncio +import logging + +from homeassistant.components.mychevy import ( + EVBinarySensorConfig, DOMAIN as MYCHEVY_DOMAIN, UPDATE_TOPIC +) +from homeassistant.components.binary_sensor import ( + ENTITY_ID_FORMAT, BinarySensorDevice) +from homeassistant.core import callback +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +SENSORS = [ + EVBinarySensorConfig("Plugged In", "plugged_in", "plug") +] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the MyChevy sensors.""" + if discovery_info is None: + return + + sensors = [] + hub = hass.data[MYCHEVY_DOMAIN] + for sconfig in SENSORS: + sensors.append(EVBinarySensor(hub, sconfig)) + + async_add_devices(sensors) + + +class EVBinarySensor(BinarySensorDevice): + """Base EVSensor class. + + The only real difference between sensors is which units and what + attribute from the car object they are returning. All logic can be + built with just setting subclass attributes. + + """ + + def __init__(self, connection, config): + """Initialize sensor with car connection.""" + self._conn = connection + self._name = config.name + self._attr = config.attr + self._type = config.device_class + self._is_on = None + + self.entity_id = ENTITY_ID_FORMAT.format( + '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def is_on(self): + """Return if on.""" + return self._is_on + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update state.""" + if self._conn.car is not None: + self._is_on = getattr(self._conn.car, self._attr, None) + self.async_schedule_update_ha_state() + + @property + def should_poll(self): + """Return the polling state.""" + return False diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 4b83f0c8f2d..19fa02f63df 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -5,21 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mysensors/ """ from homeassistant.components import mysensors -from homeassistant.components.binary_sensor import (DEVICE_CLASSES, DOMAIN, - BinarySensorDevice) +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the mysensors platform for binary sensors.""" + """Set up the MySensors platform for binary sensors.""" mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsBinarySensor, add_devices=add_devices) -class MySensorsBinarySensor( - mysensors.MySensorsEntity, BinarySensorDevice): - """Represent the value of a MySensors Binary Sensor child node.""" +class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): + """Representation of a MySensors Binary Sensor child node.""" @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index 2afaa032745..93d56a97c42 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.mystrom/ import asyncio import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN) +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY @@ -37,7 +37,7 @@ class MyStromView(HomeAssistantView): @asyncio.coroutine def get(self, request): - """The GET request received from a myStrom button.""" + """Handle the GET request received from a myStrom button.""" res = yield from self._handle(request.app['hass'], request.query) return res diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/binary_sensor/raspihats.py index ad19fb525a1..9d489a59711 100644 --- a/homeassistant/components/binary_sensor/raspihats.py +++ b/homeassistant/components/binary_sensor/raspihats.py @@ -5,18 +5,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.raspihats/ """ import logging + import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME -) -import homeassistant.helpers.config_validation as cv + from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice -) + PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.raspihats import ( - CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX, - CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException -) + CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, + CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) +from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the raspihats binary_sensor devices.""" + """Set up the raspihats binary_sensor devices.""" I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] binary_sensors = [] i2c_hat_configs = config.get(CONF_I2C_HATS) @@ -65,39 +64,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) ) except I2CHatsException as ex: - _LOGGER.error( - "Failed to register " + board + "I2CHat@" + hex(address) + " " - + str(ex) - ) + _LOGGER.error("Failed to register %s I2CHat@%s %s", + board, hex(address), str(ex)) add_devices(binary_sensors) class I2CHatBinarySensor(BinarySensorDevice): - """Represents a binary sensor that uses a I2C-HAT digital input.""" + """Representation of a binary sensor that uses a I2C-HAT digital input.""" I2C_HATS_MANAGER = None def __init__(self, address, channel, name, invert_logic, device_class): - """Initialize sensor.""" + """Initialize the raspihats sensor.""" self._address = address self._channel = channel self._name = name or DEVICE_DEFAULT_NAME self._invert_logic = invert_logic self._device_class = device_class self._state = self.I2C_HATS_MANAGER.read_di( - self._address, - self._channel - ) + self._address, self._channel) def online_callback(): - """Callback fired when board is online.""" + """Call fired when board is online.""" self.schedule_update_ha_state() self.I2C_HATS_MANAGER.register_online_callback( - self._address, - self._channel, - online_callback - ) + self._address, self._channel, online_callback) def edge_callback(state): """Read digital input state.""" @@ -105,10 +97,7 @@ class I2CHatBinarySensor(BinarySensorDevice): self.schedule_update_ha_state() self.I2C_HATS_MANAGER.register_di_callback( - self._address, - self._channel, - edge_callback - ) + self._address, self._channel, edge_callback) @property def device_class(self): @@ -122,7 +111,7 @@ class I2CHatBinarySensor(BinarySensorDevice): @property def should_poll(self): - """Polling not needed for this sensor.""" + """No polling needed for this sensor.""" return False @property diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 1c283ad214a..2cc0aee2c7b 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -1,38 +1,36 @@ """ Support for RFXtrx binary sensors. -Lighting4 devices (sensors based on PT2262 encoder) are supported and -tested. Other types may need some work. - +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rfxtrx/ """ - import logging import voluptuous as vol -from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF, CONF_NAME) from homeassistant.components import rfxtrx -from homeassistant.helpers import event as evt -from homeassistant.helpers import config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( - ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, - CONF_OFF_DELAY, CONF_DATA_BITS, CONF_DEVICES) -from homeassistant.util import slugify + ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, + CONF_FIRE_EVENT, CONF_OFF_DELAY) +from homeassistant.const import ( + CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, CONF_NAME) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import event as evt from homeassistant.util import dt as dt_util - - -DEPENDENCIES = ["rfxtrx"] +from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['rfxtrx'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=None): + DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY, default=None): vol.Any(cv.time_period, cv.positive_timedelta), @@ -45,8 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup the Binary Sensor platform to rfxtrx.""" +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Binary Sensor platform to RFXtrx.""" import RFXtrx as rfxtrxmod sensors = [] @@ -58,29 +56,26 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): continue if entity[CONF_DATA_BITS] is not None: - _LOGGER.debug("Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, - entity[CONF_DATA_BITS])) + _LOGGER.debug( + "Masked device id: %s", rfxtrx.get_pt2262_deviceid( + device_id, entity[CONF_DATA_BITS])) _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)", entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) - device = RfxtrxBinarySensor(event, entity[ATTR_NAME], - entity[CONF_DEVICE_CLASS], - entity[CONF_FIRE_EVENT], - entity[CONF_OFF_DELAY], - entity[CONF_DATA_BITS], - entity[CONF_COMMAND_ON], - entity[CONF_COMMAND_OFF]) + device = RfxtrxBinarySensor( + event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS], + entity[CONF_FIRE_EVENT], entity[CONF_OFF_DELAY], + entity[CONF_DATA_BITS], entity[CONF_COMMAND_ON], + entity[CONF_COMMAND_OFF]) device.hass = hass sensors.append(device) rfxtrx.RFX_DEVICES[device_id] = device - add_devices_callback(sensors) + add_devices(sensors) - # pylint: disable=too-many-branches def binary_sensor_update(event): - """Callback for control updates from the RFXtrx gateway.""" + """Call for control updates from the RFXtrx gateway.""" if not isinstance(event, rfxtrxmod.ControlEvent): return @@ -100,29 +95,26 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): poss_dev = rfxtrx.find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.debug("Found possible matching deviceid %s.", - poss_id) + _LOGGER.debug( + "Found possible matching device ID: %s", poss_id) pkt_id = "".join("{0:02x}".format(x) for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) sensor.hass = hass rfxtrx.RFX_DEVICES[device_id] = sensor - add_devices_callback([sensor]) - _LOGGER.info("Added binary sensor %s " - "(Device_id: %s Class: %s Sub: %s)", - pkt_id, - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype) + add_devices([sensor]) + _LOGGER.info( + "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", + pkt_id, slugify(event.device.id_string.lower()), + event.device.__class__.__name__, event.device.subtype) elif not isinstance(sensor, RfxtrxBinarySensor): return else: - _LOGGER.debug("Binary sensor update " - "(Device_id: %s Class: %s Sub: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype) + _LOGGER.debug( + "Binary sensor update (Device ID: %s Class: %s Sub: %s)", + slugify(event.device.id_string.lower()), + event.device.__class__.__name__, event.device.subtype) if sensor.is_lighting4: if sensor.data_bits is not None: @@ -142,22 +134,20 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): sensor.update_state(False) sensor.delay_listener = evt.track_point_in_time( - hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay - ) + hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay) - # Subscribe to main rfxtrx events + # Subscribe to main RFXtrx events if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) -# pylint: disable=too-many-instance-attributes,too-many-arguments class RfxtrxBinarySensor(BinarySensorDevice): - """An Rfxtrx binary sensor.""" + """A representation of a RFXtrx binary sensor.""" def __init__(self, event, name, device_class=None, should_fire=False, off_delay=None, data_bits=None, cmd_on=None, cmd_off=None): - """Initialize the sensor.""" + """Initialize the RFXtrx sensor.""" self.event = event self._name = name self._should_fire_event = should_fire @@ -172,8 +162,7 @@ class RfxtrxBinarySensor(BinarySensorDevice): if data_bits is not None: self._masked_id = rfxtrx.get_pt2262_deviceid( - event.device.id_string.lower(), - data_bits) + event.device.id_string.lower(), data_bits) else: self._masked_id = None diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/binary_sensor/rpi_pfio.py index 92d02067dfc..7acbadf873a 100644 --- a/homeassistant/components/binary_sensor/rpi_pfio.py +++ b/homeassistant/components/binary_sensor/rpi_pfio.py @@ -8,18 +8,17 @@ import logging import voluptuous as vol -import homeassistant.components.rpi_pfio as rpi_pfio from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import DEVICE_DEFAULT_NAME + PLATFORM_SCHEMA, BinarySensorDevice) +import homeassistant.components.rpi_pfio as rpi_pfio +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_NAME = 'name' -ATTR_INVERT_LOGIC = 'invert_logic' -ATTR_SETTLE_TIME = 'settle_time' +CONF_INVERT_LOGIC = 'invert_logic' CONF_PORTS = 'ports' +CONF_SETTLE_TIME = 'settle_time' DEFAULT_INVERT_LOGIC = False DEFAULT_SETTLE_TIME = 20 @@ -27,27 +26,27 @@ DEFAULT_SETTLE_TIME = 20 DEPENDENCIES = ['rpi_pfio'] PORT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_NAME, default=None): cv.string, - vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): cv.positive_int, - vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORTS, default={}): vol.Schema({ - cv.positive_int: PORT_SCHEMA + cv.positive_int: PORT_SCHEMA, }) }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the PiFace Digital Input devices.""" + """Set up the PiFace Digital Input devices.""" binary_sensors = [] - ports = config.get('ports') + ports = config.get(CONF_PORTS) for port, port_entity in ports.items(): - name = port_entity[ATTR_NAME] - settle_time = port_entity[ATTR_SETTLE_TIME] / 1000 - invert_logic = port_entity[ATTR_INVERT_LOGIC] + name = port_entity[CONF_NAME] + settle_time = port_entity[CONF_SETTLE_TIME] / 1000 + invert_logic = port_entity[CONF_INVERT_LOGIC] binary_sensors.append(RPiPFIOBinarySensor( hass, port, name, settle_time, invert_logic)) diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index a3a84580edd..95723f93870 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -4,46 +4,49 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.spc/ """ -import logging import asyncio +import logging -from homeassistant.components.spc import ( - ATTR_DISCOVER_DEVICES, DATA_REGISTRY) from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import (STATE_UNAVAILABLE, STATE_ON, STATE_OFF) - +from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE _LOGGER = logging.getLogger(__name__) -SPC_TYPE_TO_DEVICE_CLASS = {'0': 'motion', - '1': 'opening', - '3': 'smoke'} +SPC_TYPE_TO_DEVICE_CLASS = { + '0': 'motion', + '1': 'opening', + '3': 'smoke', +} - -SPC_INPUT_TO_SENSOR_STATE = {'0': STATE_OFF, - '1': STATE_ON} +SPC_INPUT_TO_SENSOR_STATE = { + '0': STATE_OFF, + '1': STATE_ON, +} def _get_device_class(spc_type): + """Get the device class.""" return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None) def _get_sensor_state(spc_input): + """Get the sensor state.""" return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE) def _create_sensor(hass, zone): - return SpcBinarySensor(zone_id=zone['id'], - name=zone['zone_name'], - state=_get_sensor_state(zone['input']), - device_class=_get_device_class(zone['type']), - spc_registry=hass.data[DATA_REGISTRY]) + """Create a SPC sensor.""" + return SpcBinarySensor( + zone_id=zone['id'], name=zone['zone_name'], + state=_get_sensor_state(zone['input']), + device_class=_get_device_class(zone['type']), + spc_registry=hass.data[DATA_REGISTRY]) @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): - """Initialize the platform.""" +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the SPC binary sensor.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None): return @@ -55,7 +58,7 @@ def async_setup_platform(hass, config, async_add_devices, class SpcBinarySensor(BinarySensorDevice): - """Represents a sensor based on an SPC zone.""" + """Representation of a sensor based on a SPC zone.""" def __init__(self, zone_id, name, state, device_class, spc_registry): """Initialize the sensor device.""" @@ -74,7 +77,7 @@ class SpcBinarySensor(BinarySensorDevice): @property def name(self): - """The name of the device.""" + """Return the name of the device.""" return self._name @property @@ -85,7 +88,7 @@ class SpcBinarySensor(BinarySensorDevice): @property def hidden(self) -> bool: """Whether the device is hidden by default.""" - # these type of sensors are probably mainly used for automations + # These type of sensors are probably mainly used for automations return True @property @@ -95,5 +98,5 @@ class SpcBinarySensor(BinarySensorDevice): @property def device_class(self): - """The device class.""" + """Return the device class.""" return self._device_class diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py index 565abb73b36..09d28b96f72 100644 --- a/homeassistant/components/binary_sensor/tapsaff.py +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -4,15 +4,15 @@ Support for Taps Affs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.tapsaff/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME) + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['tapsaff==0.1.3'] @@ -67,7 +67,7 @@ class TapsAffData(object): """Class for handling the data retrieval for pins.""" def __init__(self, location): - """Initialize the sensor.""" + """Initialize the data object.""" from tapsaff import TapsAff self._is_taps_aff = None diff --git a/homeassistant/components/binary_sensor/tesla.py b/homeassistant/components/binary_sensor/tesla.py index a7cda90b3f6..3d494a28ea8 100644 --- a/homeassistant/components/binary_sensor/tesla.py +++ b/homeassistant/components/binary_sensor/tesla.py @@ -28,7 +28,7 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): """Implement an Tesla binary sensor for parking and charger.""" def __init__(self, tesla_device, controller, sensor_type): - """Initialisation of binary sensor.""" + """Initialise of a Tesla binary sensor.""" super().__init__(tesla_device, controller) self._state = False self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) diff --git a/homeassistant/components/binary_sensor/verisure.py b/homeassistant/components/binary_sensor/verisure.py index 8702c8bd770..4a1b99f4b9b 100644 --- a/homeassistant/components/binary_sensor/verisure.py +++ b/homeassistant/components/binary_sensor/verisure.py @@ -6,15 +6,15 @@ https://home-assistant.io/components/binary_sensor.verisure/ """ import logging -from homeassistant.components.verisure import HUB as hub from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.verisure import CONF_DOOR_WINDOW +from homeassistant.components.verisure import HUB as hub _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Verisure binary sensors.""" + """Set up the Verisure binary sensors.""" sensors = [] hub.update_overview() @@ -27,10 +27,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VerisureDoorWindowSensor(BinarySensorDevice): - """Verisure door window sensor.""" + """Representation of a Verisure door window sensor.""" def __init__(self, device_label): - """Initialize the modbus coil sensor.""" + """Initialize the Verisure door window sensor.""" self._device_label = device_label @property diff --git a/homeassistant/components/binary_sensor/vultr.py b/homeassistant/components/binary_sensor/vultr.py index 66b5a127be1..eecd3f87c40 100644 --- a/homeassistant/components/binary_sensor/vultr.py +++ b/homeassistant/components/binary_sensor/vultr.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Vultr subscription (server) sensor.""" + """Set up the Vultr subscription (server) binary sensor.""" vultr = hass.data[DATA_VULTR] subscription = config.get(CONF_SUBSCRIPTION) @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if subscription not in vultr.data: _LOGGER.error("Subscription %s not found", subscription) - return False + return add_devices([VultrBinarySensor(vultr, subscription, name)], True) @@ -48,7 +48,7 @@ class VultrBinarySensor(BinarySensorDevice): """Representation of a Vultr subscription sensor.""" def __init__(self, vultr, subscription, name): - """Initialize a new Vultr sensor.""" + """Initialize a new Vultr binary sensor.""" self._vultr = vultr self._name = name diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index e0bf23ecee2..575507cd047 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -8,7 +8,7 @@ import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.wink import WinkDevice, DOMAIN +from homeassistant.components.wink import DOMAIN, WinkDevice _LOGGER = logging.getLogger(__name__) @@ -16,18 +16,18 @@ DEPENDENCIES = ['wink'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { - 'opened': 'opening', 'brightness': 'light', - 'vibration': 'vibration', - 'loudness': 'sound', - 'noise': 'sound', 'capturing_audio': 'sound', - 'liquid_detected': 'moisture', - 'motion': 'motion', - 'presence': 'occupancy', + 'capturing_video': None, 'co_detected': 'gas', + 'liquid_detected': 'moisture', + 'loudness': 'sound', + 'motion': 'motion', + 'noise': 'sound', + 'opened': 'opening', + 'presence': 'occupancy', 'smoke_detected': 'smoke', - 'capturing_video': None + 'vibration': 'vibration', } @@ -103,7 +103,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self) @property @@ -118,7 +118,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): @property def device_state_attributes(self): - """Return the state attributes.""" + """Return the device state attributes.""" return super().device_state_attributes @@ -127,7 +127,7 @@ class WinkSmokeDetector(WinkBinarySensorDevice): @property def device_state_attributes(self): - """Return the state attributes.""" + """Return the device state attributes.""" _attributes = super().device_state_attributes _attributes['test_activated'] = self.wink.test_activated() return _attributes @@ -138,11 +138,18 @@ class WinkHub(WinkBinarySensorDevice): @property def device_state_attributes(self): - """Return the state attributes.""" + """Return the device state attributes.""" _attributes = super().device_state_attributes _attributes['update_needed'] = self.wink.update_needed() _attributes['firmware_version'] = self.wink.firmware_version() _attributes['pairing_mode'] = self.wink.pairing_mode() + _kidde_code = self.wink.kidde_radio_code() + if _kidde_code is not None: + # The service call to set the Kidde code + # takes a string of 1s and 0s so it makes + # sense to display it to the user that way + _formatted_kidde_code = "{:b}".format(_kidde_code).zfill(8) + _attributes['kidde_radio_code'] = _formatted_kidde_code return _attributes @@ -170,7 +177,7 @@ class WinkButton(WinkBinarySensorDevice): @property def device_state_attributes(self): - """Return the state attributes.""" + """Return the device state attributes.""" _attributes = super().device_state_attributes _attributes['pressed'] = self.wink.pressed() _attributes['long_pressed'] = self.wink.long_pressed() diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 83dc51a2e0f..af814cfd464 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,17 +17,21 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.8.1'] +REQUIREMENTS = ['holidays==0.9.3'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime -ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA', - 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England', - 'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE', - 'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL', - 'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO', - 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain', - 'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales'] +ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', + 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', + 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', + 'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', + 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', + 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', + 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', + 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', + 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', + 'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', + 'Wales'] CONF_COUNTRY = 'country' CONF_PROVINCE = 'province' CONF_WORKDAYS = 'workdays' diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index eee24b8ad1c..99037f60107 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -101,7 +101,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: self._density = int(data.get(DENSITY)) @@ -139,8 +139,16 @@ class XiaomiMotionSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" + if raw_data['cmd'] == 'heartbeat': + _LOGGER.debug( + 'Skipping heartbeat of the motion sensor. ' + 'It can introduce an incorrect state because of a firmware ' + 'bug (https://github.com/home-assistant/home-assistant/pull/' + '11631#issuecomment-357507744).') + return + self._should_poll = False if NO_MOTION in data: # handle push from the hub self._no_motion_since = data[NO_MOTION] @@ -186,7 +194,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False if NO_CLOSE in data: # handle push from the hub @@ -219,7 +227,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor', xiaomi_hub, 'status', 'moisture') - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False @@ -256,7 +264,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: self._density = int(data.get(DENSITY)) @@ -293,7 +301,7 @@ class XiaomiButton(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) if value is None: @@ -343,7 +351,7 @@ class XiaomiCube(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if 'status' in data: self._hass.bus.fire('cube_action', { diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 36894dcab61..8b2401aa589 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -4,18 +4,18 @@ Support for WebDav Calendar. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/calendar.caldav/ """ +from datetime import datetime, timedelta import logging import re -from datetime import datetime, timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.calendar import ( - CalendarEventDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, CalendarEventDevice) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME) -from homeassistant.util import dt, Throttle +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle, dt REQUIREMENTS = ['caldav==0.5.0'] @@ -39,9 +39,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_CUSTOM_CALENDARS, default=[]): vol.All(cv.ensure_list, vol.Schema([ vol.Schema({ - vol.Required(CONF_NAME): cv.string, vol.Required(CONF_CALENDAR): cv.string, - vol.Required(CONF_SEARCH): cv.string + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SEARCH): cv.string, }) ])) }) @@ -53,12 +53,12 @@ def setup_platform(hass, config, add_devices, disc_info=None): """Set up the WebDav Calendar platform.""" import caldav - client = caldav.DAVClient(config.get(CONF_URL), - None, - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + url = config.get(CONF_URL) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + client = caldav.DAVClient(url, None, username, password) - # Retrieve all the remote calendars calendars = client.principal().calendars() calendar_devices = [] @@ -70,8 +70,7 @@ def setup_platform(hass, config, add_devices, disc_info=None): _LOGGER.debug("Ignoring calendar '%s'", calendar.name) continue - # Create additional calendars based on custom filtering - # rules + # Create additional calendars based on custom filtering rules for cust_calendar in config.get(CONF_CUSTOM_CALENDARS): # Check that the base calendar matches if cust_calendar.get(CONF_CALENDAR) != calendar.name: @@ -85,12 +84,9 @@ def setup_platform(hass, config, add_devices, disc_info=None): } calendar_devices.append( - WebDavCalendarEventDevice(hass, - device_data, - calendar, - True, - cust_calendar.get(CONF_SEARCH)) - ) + WebDavCalendarEventDevice( + hass, device_data, calendar, True, + cust_calendar.get(CONF_SEARCH))) # Create a default calendar if there was no custom one if not config.get(CONF_CUSTOM_CALENDARS): @@ -102,18 +98,13 @@ def setup_platform(hass, config, add_devices, disc_info=None): WebDavCalendarEventDevice(hass, device_data, calendar) ) - # Finally add all the calendars we've created add_devices(calendar_devices) class WebDavCalendarEventDevice(CalendarEventDevice): """A device for getting the next Task from a WebDav Calendar.""" - def __init__(self, - hass, - device_data, - calendar, - all_day=False, + def __init__(self, hass, device_data, calendar, all_day=False, search=None): """Create the WebDav Calendar Event Device.""" self.data = WebDavCalendarData(calendar, all_day, search) @@ -167,9 +158,7 @@ class WebDavCalendarData(object): if vevent is None: _LOGGER.debug( "No matching event found in the %d results for %s", - len(results), - self.calendar.name, - ) + len(results), self.calendar.name) self.event = None return True diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index ecf8bfb7cf7..81191e3025e 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -4,29 +4,28 @@ Support for Todoist task management (https://todoist.com). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/calendar.todoist/ """ - - -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta import logging import voluptuous as vol from homeassistant.components.calendar import ( - CalendarEventDevice, DOMAIN, PLATFORM_SCHEMA) -from homeassistant.components.google import ( - CONF_DEVICE_ID) -from homeassistant.const import ( - CONF_ID, CONF_NAME, CONF_TOKEN) + DOMAIN, PLATFORM_SCHEMA, CalendarEventDevice) +from homeassistant.components.google import CONF_DEVICE_ID +from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.util import dt -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt REQUIREMENTS = ['todoist-python==7.0.17'] _LOGGER = logging.getLogger(__name__) +CONF_EXTRA_PROJECTS = 'custom_projects' +CONF_PROJECT_DUE_DATE = 'due_date_days' +CONF_PROJECT_LABEL_WHITELIST = 'labels' +CONF_PROJECT_WHITELIST = 'include_projects' + # Calendar Platform: Does this calendar event last all day? ALL_DAY = 'all_day' # Attribute: All tasks in this project @@ -78,20 +77,15 @@ SUMMARY = 'summary' TASKS = 'items' SERVICE_NEW_TASK = 'todoist_new_task' + NEW_TASK_SERVICE_SCHEMA = vol.Schema({ vol.Required(CONTENT): cv.string, 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.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), + vol.Optional(DUE_DATE): cv.string, }) -CONF_EXTRA_PROJECTS = 'custom_projects' -CONF_PROJECT_DUE_DATE = 'due_date_days' -CONF_PROJECT_WHITELIST = 'include_projects' -CONF_PROJECT_LABEL_WHITELIST = 'labels' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_EXTRA_PROJECTS, default=[]): @@ -111,8 +105,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Todoist platform.""" - # Check token: + """Set up the Todoist platform.""" token = config.get(CONF_TOKEN) # Look up IDs based on (lowercase) names. @@ -176,7 +169,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(project_devices) def handle_new_task(call): - """Called when a user creates a new Todoist Task from HASS.""" + """Call when a user creates a new Todoist Task from HASS.""" project_name = call.data[PROJECT_NAME] project_id = project_id_lookup[project_name] @@ -528,8 +521,7 @@ class TodoistProjectData(object): # Let's set our "due date" to tomorrow self.event[END] = { DATETIME: ( - datetime.utcnow() + - timedelta(days=1) + datetime.utcnow() + timedelta(days=1) ).strftime(DATE_STR_FORMAT) } _LOGGER.debug("Updated %s", self._name) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 6839c2c3b9c..1bb88050b2f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -124,15 +124,15 @@ def async_setup(hass, config): """Set up the camera component.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - hass.http.register_view(CameraImageView(component.entities)) - hass.http.register_view(CameraMjpegStream(component.entities)) + hass.http.register_view(CameraImageView(component)) + hass.http.register_view(CameraMjpegStream(component)) yield from component.async_setup(config) @callback def update_tokens(time): """Update tokens of the entities.""" - for entity in component.entities.values(): + for entity in component.entities: entity.async_update_token() hass.async_add_job(entity.async_update_ha_state()) @@ -358,14 +358,14 @@ class CameraView(HomeAssistantView): requires_auth = False - def __init__(self, entities): + def __init__(self, component): """Initialize a basic camera view.""" - self.entities = entities + self.component = component @asyncio.coroutine def get(self, request, entity_id): """Start a GET request.""" - camera = self.entities.get(entity_id) + camera = self.component.get_entity(entity_id) if camera is None: status = 404 if request[KEY_AUTHENTICATED] else 401 diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index 4f597771726..4d461b0e0b5 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -75,7 +75,9 @@ class ArloCam(Camera): self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_refresh = None - self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds() + if self._camera.base_station: + self._camera.base_station.refresh_rate = \ + SCAN_INTERVAL.total_seconds() self.attrs = {} def camera_image(self): diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py index 492c2a47729..51c3bc89b05 100644 --- a/homeassistant/components/camera/axis.py +++ b/homeassistant/components/camera/axis.py @@ -6,11 +6,11 @@ https://home-assistant.io/components/camera.axis/ """ import logging -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, - CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.components.camera.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) +from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) from homeassistant.helpers.dispatcher import dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -20,6 +20,7 @@ DEPENDENCIES = [DOMAIN] def _get_image_url(host, port, mode): + """Set the URL to get the image.""" if mode == 'mjpeg': return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port) elif mode == 'single': @@ -27,34 +28,32 @@ def _get_image_url(host, port, mode): def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Axis camera.""" + """Set up the Axis camera.""" camera_config = { CONF_NAME: discovery_info[CONF_NAME], CONF_USERNAME: discovery_info[CONF_USERNAME], CONF_PASSWORD: discovery_info[CONF_PASSWORD], - CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], - str(discovery_info[CONF_PORT]), - 'mjpeg'), - CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST], - str(discovery_info[CONF_PORT]), - 'single'), + CONF_MJPEG_URL: _get_image_url( + discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), + 'mjpeg'), + CONF_STILL_IMAGE_URL: _get_image_url( + discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]), + 'single'), CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, } - add_devices([AxisCamera(hass, - camera_config, - str(discovery_info[CONF_PORT]))]) + add_devices([AxisCamera( + hass, camera_config, str(discovery_info[CONF_PORT]))]) class AxisCamera(MjpegCamera): - """AxisCamera class.""" + """Representation of a Axis camera.""" def __init__(self, hass, config, port): """Initialize Axis Communications camera component.""" super().__init__(hass, config) self.port = port - dispatcher_connect(hass, - DOMAIN + '_' + config[CONF_NAME] + '_new_ip', - self._new_ip) + dispatcher_connect( + hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip) def _new_ip(self, host): """Set new IP for video stream.""" diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py index 4b708817cfd..8475ca8fd1e 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/camera/blink.py @@ -4,21 +4,21 @@ Support for Blink system camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.blink/ """ +from datetime import timedelta import logging -from datetime import timedelta import requests from homeassistant.components.blink import DOMAIN from homeassistant.components.camera import Camera from homeassistant.util import Throttle +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['blink'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Blink Camera.""" @@ -45,7 +45,7 @@ class BlinkCamera(Camera): self.notifications = self.data.cameras[self._name].notifications self.response = None - _LOGGER.info("Initialized blink camera %s", self._name) + _LOGGER.debug("Initialized blink camera %s", self._name) @property def name(self): @@ -55,7 +55,7 @@ class BlinkCamera(Camera): @Throttle(MIN_TIME_BETWEEN_UPDATES) def request_image(self): """Request a new image from Blink servers.""" - _LOGGER.info("Requesting new image from blink servers") + _LOGGER.debug("Requesting new image from blink servers") image_url = self.check_for_motion() header = self.data.cameras[self._name].header self.response = requests.get(image_url, headers=header, stream=True) @@ -68,7 +68,7 @@ class BlinkCamera(Camera): # We detected motion at some point self.data.last_motion() self.notifications = notifs - # returning motion image currently not working + # Returning motion image currently not working # return self.data.cameras[self._name].motion['image'] elif notifs < self.notifications: self.notifications = notifs diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 3cc391eae33..15db83d345a 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -44,6 +44,8 @@ class FoscamCam(Camera): def __init__(self, device_info): """Initialize a Foscam camera.""" + from libpyfoscam import FoscamCamera + super(FoscamCam, self).__init__() ip_address = device_info.get(CONF_IP) @@ -53,10 +55,8 @@ class FoscamCam(Camera): self._name = device_info.get(CONF_NAME) self._motion_status = False - from libpyfoscam import FoscamCamera - - self._foscam_session = FoscamCamera(ip_address, port, self._username, - self._password, verbose=False) + self._foscam_session = FoscamCamera( + ip_address, port, self._username, self._password, verbose=False) def camera_image(self): """Return a still image response from the camera.""" @@ -75,20 +75,20 @@ class FoscamCam(Camera): def enable_motion_detection(self): """Enable motion detection in camera.""" - ret, err = self._foscam_session.enable_motion_detection() - if ret == FOSCAM_COMM_ERROR: - _LOGGER.debug("Unable to communicate with Foscam Camera: %s", err) - self._motion_status = True - else: + try: + ret = self._foscam_session.enable_motion_detection() + self._motion_status = ret == FOSCAM_COMM_ERROR + except TypeError: + _LOGGER.debug("Communication problem") self._motion_status = False def disable_motion_detection(self): """Disable motion detection.""" - ret, err = self._foscam_session.disable_motion_detection() - if ret == FOSCAM_COMM_ERROR: - _LOGGER.debug("Unable to communicate with Foscam Camera: %s", err) - self._motion_status = True - else: + try: + ret = self._foscam_session.disable_motion_detection() + self._motion_status = ret == FOSCAM_COMM_ERROR + except TypeError: + _LOGGER.debug("Communication problem") self._motion_status = False @property diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 8f30d9c8b8f..65f291bf41d 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( - DATA_FFMPEG) + DATA_FFMPEG, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream) @@ -31,6 +31,7 @@ DEFAULT_NAME = 'ONVIF Camera' DEFAULT_PORT = 5000 DEFAULT_USERNAME = 'admin' DEFAULT_PASSWORD = '888888' +DEFAULT_ARGUMENTS = '-q:v 2' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -38,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, }) @@ -59,7 +61,7 @@ class ONVIFCamera(Camera): super().__init__() self._name = config.get(CONF_NAME) - self._ffmpeg_arguments = '-q:v 2' + self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) media = ONVIFService( 'http://{}:{}/onvif/device_service'.format( config.get(CONF_HOST), config.get(CONF_PORT)), diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py new file mode 100755 index 00000000000..72f40cb83a4 --- /dev/null +++ b/homeassistant/components/camera/xeoma.py @@ -0,0 +1,113 @@ +""" +Support for Xeoma Cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.xeoma/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.helpers import config_validation as cv + +REQUIREMENTS = ['pyxeoma==1.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CAMERAS = 'cameras' +CONF_HIDE = 'hide' +CONF_IMAGE_NAME = 'image_name' +CONF_NEW_VERSION = 'new_version' + +CAMERAS_SCHEMA = vol.Schema({ + vol.Required(CONF_IMAGE_NAME): cv.string, + vol.Optional(CONF_HIDE, default=False): cv.boolean, + vol.Optional(CONF_NAME): cv.string, +}, required=False) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_CAMERAS, default={}): + vol.Schema(vol.All(cv.ensure_list, [CAMERAS_SCHEMA])), + vol.Optional(CONF_NEW_VERSION, default=True): cv.boolean, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, +}) + + +@asyncio.coroutine +# pylint: disable=unused-argument +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Discover and setup Xeoma Cameras.""" + from pyxeoma.xeoma import Xeoma, XeomaError + + host = config[CONF_HOST] + login = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + new_version = config[CONF_NEW_VERSION] + + xeoma = Xeoma(host, new_version, login, password) + + try: + yield from xeoma.async_test_connection() + discovered_image_names = yield from xeoma.async_get_image_names() + discovered_cameras = [ + { + CONF_IMAGE_NAME: image_name, + CONF_HIDE: False, + CONF_NAME: image_name + } + for image_name in discovered_image_names + ] + + for cam in config[CONF_CAMERAS]: + camera = next( + (dc for dc in discovered_cameras + if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None) + + if camera is not None: + if CONF_NAME in cam: + camera[CONF_NAME] = cam[CONF_NAME] + if CONF_HIDE in cam: + camera[CONF_HIDE] = cam[CONF_HIDE] + + cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras)) + async_add_devices( + [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME]) + for camera in cameras]) + except XeomaError as err: + _LOGGER.error("Error: %s", err.message) + return + + +class XeomaCamera(Camera): + """Implementation of a Xeoma camera.""" + + def __init__(self, xeoma, image, name): + """Initialize a Xeoma camera.""" + super().__init__() + self._xeoma = xeoma + self._name = name + self._image = image + self._last_image = None + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + from pyxeoma.xeoma import XeomaError + try: + image = yield from self._xeoma.async_get_camera_image(self._image) + self._last_image = image + except XeomaError as err: + _LOGGER.error("Error fetching image: %s", err.message) + + return self._last_image + + @property + def name(self): + """Return the name of this device.""" + return self._name diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index bb714ad5f81..ce656eb96e8 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -499,53 +499,54 @@ class ClimateDevice(Entity): self.precision), } + supported_features = self.supported_features if self.target_temperature_step is not None: data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step - target_temp_high = self.target_temperature_high - if target_temp_high is not None: + if supported_features & SUPPORT_TARGET_TEMPERATURE_HIGH: data[ATTR_TARGET_TEMP_HIGH] = show_temp( self.hass, self.target_temperature_high, self.temperature_unit, self.precision) + + if supported_features & SUPPORT_TARGET_TEMPERATURE_LOW: data[ATTR_TARGET_TEMP_LOW] = show_temp( self.hass, self.target_temperature_low, self.temperature_unit, self.precision) - humidity = self.target_humidity - if humidity is not None: - data[ATTR_HUMIDITY] = humidity + if supported_features & SUPPORT_TARGET_HUMIDITY: + data[ATTR_HUMIDITY] = self.target_humidity data[ATTR_CURRENT_HUMIDITY] = self.current_humidity - data[ATTR_MIN_HUMIDITY] = self.min_humidity - data[ATTR_MAX_HUMIDITY] = self.max_humidity - fan_mode = self.current_fan_mode - if fan_mode is not None: - data[ATTR_FAN_MODE] = fan_mode + if supported_features & SUPPORT_TARGET_HUMIDITY_LOW: + data[ATTR_MIN_HUMIDITY] = self.min_humidity + + if supported_features & SUPPORT_TARGET_HUMIDITY_HIGH: + data[ATTR_MAX_HUMIDITY] = self.max_humidity + + if supported_features & SUPPORT_FAN_MODE: + data[ATTR_FAN_MODE] = self.current_fan_mode if self.fan_list: data[ATTR_FAN_LIST] = self.fan_list - operation_mode = self.current_operation - if operation_mode is not None: - data[ATTR_OPERATION_MODE] = operation_mode + if supported_features & SUPPORT_OPERATION_MODE: + data[ATTR_OPERATION_MODE] = self.current_operation if self.operation_list: data[ATTR_OPERATION_LIST] = self.operation_list - is_hold = self.current_hold_mode - if is_hold is not None: - data[ATTR_HOLD_MODE] = is_hold + if supported_features & SUPPORT_HOLD_MODE: + data[ATTR_HOLD_MODE] = self.current_hold_mode - swing_mode = self.current_swing_mode - if swing_mode is not None: - data[ATTR_SWING_MODE] = swing_mode + if supported_features & SUPPORT_SWING_MODE: + data[ATTR_SWING_MODE] = self.current_swing_mode if self.swing_list: data[ATTR_SWING_LIST] = self.swing_list - is_away = self.is_away_mode_on - if is_away is not None: + if supported_features & SUPPORT_AWAY_MODE: + is_away = self.is_away_mode_on data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF - is_aux_heat = self.is_aux_heat_on - if is_aux_heat is not None: + if supported_features & SUPPORT_AUX_HEAT: + is_aux_heat = self.is_aux_heat_on data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF return data diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index 8f6df034b89..1f38fdf3c82 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -9,35 +9,23 @@ import re import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE, - ATTR_CURRENT_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, - STATE_DRY, STATE_FAN_ONLY -) + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, + ATTR_SWING_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + ClimateDevice) from homeassistant.components.daikin import ( - daikin_api_setup, - ATTR_TARGET_TEMPERATURE, - ATTR_INSIDE_TEMPERATURE, - ATTR_OUTSIDE_TEMPERATURE -) + ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE, + daikin_api_setup) from homeassistant.const import ( - CONF_HOST, CONF_NAME, - TEMP_CELSIUS, - ATTR_TEMPERATURE -) + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pydaikin==0.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | - SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE | - SUPPORT_SWING_MODE) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=None): cv.string, @@ -56,19 +44,22 @@ HA_ATTR_TO_DAIKIN = { ATTR_OPERATION_MODE: 'mode', ATTR_FAN_MODE: 'f_rate', ATTR_SWING_MODE: 'f_dir', + ATTR_INSIDE_TEMPERATURE: 'htemp', + ATTR_OUTSIDE_TEMPERATURE: 'otemp', + ATTR_TARGET_TEMPERATURE: 'stemp' } def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Daikin HVAC platform.""" + """Set up the Daikin HVAC platform.""" if discovery_info is not None: host = discovery_info.get('ip') name = None - _LOGGER.info("Discovered a Daikin AC on %s", host) + _LOGGER.debug("Discovered a Daikin AC on %s", host) else: host = config.get(CONF_HOST) name = config.get(CONF_NAME) - _LOGGER.info("Added Daikin AC on %s", host) + _LOGGER.debug("Added Daikin AC on %s", host) api = daikin_api_setup(hass, host, name) add_devices([DaikinClimate(api)], True) @@ -101,6 +92,17 @@ class DaikinClimate(ClimateDevice): ), } + self._supported_features = SUPPORT_TARGET_TEMPERATURE \ + | SUPPORT_OPERATION_MODE + + daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE] + if self._api.device.values.get(daikin_attr) is not None: + self._supported_features |= SUPPORT_FAN_MODE + + daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE] + if self._api.device.values.get(daikin_attr) is not None: + self._supported_features |= SUPPORT_SWING_MODE + def get(self, key): """Retrieve device settings from API library cache.""" value = None @@ -108,29 +110,34 @@ class DaikinClimate(ClimateDevice): if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE, ATTR_CURRENT_TEMPERATURE]: - value = self._api.device.values.get('htemp') + key = ATTR_INSIDE_TEMPERATURE + + daikin_attr = HA_ATTR_TO_DAIKIN.get(key) + + if key == ATTR_INSIDE_TEMPERATURE: + value = self._api.device.values.get(daikin_attr) cast_to_float = True - if key == ATTR_TARGET_TEMPERATURE: - value = self._api.device.values.get('stemp') + elif key == ATTR_TARGET_TEMPERATURE: + value = self._api.device.values.get(daikin_attr) cast_to_float = True elif key == ATTR_OUTSIDE_TEMPERATURE: - value = self._api.device.values.get('otemp') + value = self._api.device.values.get(daikin_attr) cast_to_float = True elif key == ATTR_FAN_MODE: - value = self._api.device.represent('f_rate')[1].title() + value = self._api.device.represent(daikin_attr)[1].title() elif key == ATTR_SWING_MODE: - value = self._api.device.represent('f_dir')[1].title() + value = self._api.device.represent(daikin_attr)[1].title() elif key == ATTR_OPERATION_MODE: # Daikin can return also internal states auto-1 or auto-7 # and we need to translate them as AUTO value = re.sub( '[^a-z]', '', - self._api.device.represent('mode')[1] + self._api.device.represent(daikin_attr)[1] ).title() if value is None: - _LOGGER.warning("Invalid value requested for key %s", key) + _LOGGER.error("Invalid value requested for key %s", key) else: if value == "-" or value == "--": value = None @@ -178,7 +185,7 @@ class DaikinClimate(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._supported_features @property def name(self): diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 2fe6ba0c874..c3ba523468f 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/demo/ from homeassistant.components.climate import ( ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, @@ -14,6 +15,7 @@ from homeassistant.components.climate import ( from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | + SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py index 5620bcbfa11..bb92a92467a 100644 --- a/homeassistant/components/climate/econet.py +++ b/homeassistant/components/climate/econet.py @@ -10,16 +10,12 @@ import logging import voluptuous as vol from homeassistant.components.climate import ( - DOMAIN, - PLATFORM_SCHEMA, - STATE_ECO, STATE_GAS, STATE_ELECTRIC, - STATE_HEAT_PUMP, STATE_HIGH_DEMAND, - STATE_OFF, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_OPERATION_MODE, - ClimateDevice) -from homeassistant.const import (ATTR_ENTITY_ID, - CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE) + DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, STATE_GAS, + STATE_HEAT_PUMP, STATE_HIGH_DEMAND, STATE_OFF, STATE_PERFORMANCE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME, + TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyeconet==0.0.4'] @@ -59,6 +55,7 @@ HA_STATE_TO_ECONET = { STATE_GAS: 'gas', STATE_HIGH_DEMAND: 'High Demand', STATE_OFF: 'Off', + STATE_PERFORMANCE: 'Performance' } ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()} @@ -87,7 +84,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.data[ECONET_DATA]['water_heaters'].extend(hass_water_heaters) def service_handle(service): - """Handler for services.""" + """Handle the service calls.""" entity_ids = service.data.get('entity_id') all_heaters = hass.data[ECONET_DATA]['water_heaters'] _heaters = [ @@ -105,12 +102,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _water_heater.schedule_update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_ADD_VACATION, - service_handle, + hass.services.register(DOMAIN, SERVICE_ADD_VACATION, service_handle, schema=ADD_VACATION_SCHEMA) - hass.services.register(DOMAIN, SERVICE_DELETE_VACATION, - service_handle, + hass.services.register(DOMAIN, SERVICE_DELETE_VACATION, service_handle, schema=DELETE_VACATION_SCHEMA) @@ -138,7 +133,7 @@ class EcoNetWaterHeater(ClimateDevice): @property def device_state_attributes(self): - """Return the optional state attributes.""" + """Return the optional device state attributes.""" data = {} vacations = self.water_heater.get_vacations() if vacations: @@ -157,8 +152,7 @@ class EcoNetWaterHeater(ClimateDevice): """ Return current operation as one of the following. - ["eco", "heat_pump", - "high_demand", "electric_only"] + ["eco", "heat_pump", "high_demand", "electric_only"] """ current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode) return current_op @@ -189,7 +183,7 @@ class EcoNetWaterHeater(ClimateDevice): if target_temp is not None: self.water_heater.set_target_set_point(target_temp) else: - _LOGGER.error("A target temperature must be provided.") + _LOGGER.error("A target temperature must be provided") def set_operation_mode(self, operation_mode): """Set operation mode.""" @@ -197,7 +191,7 @@ class EcoNetWaterHeater(ClimateDevice): if op_mode_to_set is not None: self.water_heater.set_mode(op_mode_to_set) else: - _LOGGER.error("An operation mode must be provided.") + _LOGGER.error("An operation mode must be provided") def add_vacation(self, start, end): """Add a vacation to this water heater.""" diff --git a/homeassistant/components/climate/ephember.py b/homeassistant/components/climate/ephember.py index a1d11bce901..e1f1ab7d448 100644 --- a/homeassistant/components/climate/ephember.py +++ b/homeassistant/components/climate/ephember.py @@ -9,9 +9,10 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT) + ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD) + TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyephember==0.1.1'] @@ -59,7 +60,10 @@ class EphEmberThermostat(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_AUX_HEAT + if self._hot_water: + return SUPPORT_AUX_HEAT + + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_AUX_HEAT @property def name(self): @@ -81,6 +85,14 @@ class EphEmberThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._zone['targetTemperature'] + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + if self._hot_water: + return None + + return 1 + @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" @@ -105,17 +117,38 @@ class EphEmberThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" - return + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + if self._hot_water: + return + + if temperature == self.target_temperature: + return + + if temperature > self.max_temp or temperature < self.min_temp: + return + + self._ember.set_target_temperture_by_name(self._zone_name, + int(temperature)) @property def min_temp(self): """Return the minimum temperature.""" - return self._zone['targetTemperature'] + # Hot water temp doesn't support being changed + if self._hot_water: + return self._zone['targetTemperature'] + + return 5 @property def max_temp(self): """Return the maximum temperature.""" - return self._zone['targetTemperature'] + if self._hot_water: + return self._zone['targetTemperature'] + + return 35 def update(self): """Get the latest data.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index eb9b5c5ba6e..9b3b7d650a9 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.6'] +REQUIREMENTS = ['python-eq3bt==0.1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index fdfe56ca62c..9445fc7cfc9 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -17,7 +17,8 @@ from homeassistant.components.climate import ( SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, - CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, + STATE_UNKNOWN) from homeassistant.helpers import condition from homeassistant.helpers.event import ( async_track_state_change, async_track_time_interval) @@ -30,7 +31,6 @@ DEPENDENCIES = ['switch', 'sensor'] DEFAULT_TOLERANCE = 0.3 DEFAULT_NAME = 'Generic Thermostat' -DEFAULT_AWAY_TEMP = 16 CONF_HEATER = 'heater' CONF_SENSOR = 'target_sensor' @@ -44,7 +44,7 @@ CONF_HOT_TOLERANCE = 'hot_tolerance' CONF_KEEP_ALIVE = 'keep_alive' CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode' CONF_AWAY_TEMP = 'away_temp' -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -64,8 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ cv.time_period, cv.positive_timedelta), vol.Optional(CONF_INITIAL_OPERATION_MODE): vol.In([STATE_AUTO, STATE_OFF]), - vol.Optional(CONF_AWAY_TEMP, - default=DEFAULT_AWAY_TEMP): vol.Coerce(float) + vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float) }) @@ -119,6 +118,7 @@ class GenericThermostat(ClimateDevice): self._operation_list = [STATE_HEAT, STATE_OFF] if initial_operation_mode == STATE_OFF: self._enabled = False + self._current_operation = STATE_OFF else: self._enabled = True self._active = False @@ -127,6 +127,9 @@ class GenericThermostat(ClimateDevice): self._max_temp = max_temp self._target_temp = target_temp self._unit = hass.config.units.temperature_unit + self._support_flags = SUPPORT_FLAGS + if away_temp is not None: + self._support_flags = SUPPORT_FLAGS | SUPPORT_AWAY_MODE self._away_temp = away_temp self._is_away = False @@ -139,6 +142,10 @@ class GenericThermostat(ClimateDevice): async_track_time_interval( hass, self._async_keep_alive, self._keep_alive) + sensor_state = hass.states.get(sensor_entity_id) + if sensor_state and sensor_state.state != STATE_UNKNOWN: + self._async_update_temp(sensor_state) + @asyncio.coroutine def async_added_to_hass(self): """Run when entity about to be added.""" @@ -154,19 +161,29 @@ class GenericThermostat(ClimateDevice): self._target_temp = self.max_temp else: self._target_temp = self.min_temp - _LOGGER.warning('Undefined target temperature, \ - falling back to %s', self._target_temp) + _LOGGER.warning("Undefined target temperature," + "falling back to %s", self._target_temp) else: self._target_temp = float( old_state.attributes[ATTR_TEMPERATURE]) - self._is_away = True if str( - old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False - if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: - self._current_operation = STATE_OFF - self._enabled = False - if self._initial_operation_mode is None: - if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF: - self._enabled = False + if old_state.attributes[ATTR_AWAY_MODE] is not None: + self._is_away = str( + old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON + if (self._initial_operation_mode is None and + old_state.attributes[ATTR_OPERATION_MODE] is not None): + self._current_operation = \ + old_state.attributes[ATTR_OPERATION_MODE] + if self._current_operation != STATE_OFF: + self._enabled = True + else: + # No previous state, try and restore defaults + if self._target_temp is None: + if self.ac_mode: + self._target_temp = self.max_temp + else: + self._target_temp = self.min_temp + _LOGGER.warning("No previously saved temperature, setting to %s", + self._target_temp) @property def state(self): @@ -230,7 +247,7 @@ class GenericThermostat(ClimateDevice): if self._is_device_active: self._heater_turn_off() else: - _LOGGER.error('Unrecognized operation mode: %s', operation_mode) + _LOGGER.error("Unrecognized operation mode: %s", operation_mode) return # Ensure we updae the current operation after changing the mode self.schedule_update_ha_state() @@ -299,7 +316,7 @@ class GenericThermostat(ClimateDevice): self._cur_temp = self.hass.config.units.temperature( float(state.state), unit) except ValueError as ex: - _LOGGER.error('Unable to update from sensor: %s', ex) + _LOGGER.error("Unable to update from sensor: %s", ex) @callback def _async_control_heating(self): @@ -307,8 +324,9 @@ class GenericThermostat(ClimateDevice): if not self._active and None not in (self._cur_temp, self._target_temp): self._active = True - _LOGGER.info('Obtained current and target temperature. ' - 'Generic thermostat active.') + _LOGGER.info("Obtained current and target temperature. " + "Generic thermostat active. %s, %s", + self._cur_temp, self._target_temp) if not self._active: return @@ -333,13 +351,13 @@ class GenericThermostat(ClimateDevice): too_cold = self._target_temp - self._cur_temp >= \ self._cold_tolerance if too_cold: - _LOGGER.info('Turning off AC %s', self.heater_entity_id) + _LOGGER.info("Turning off AC %s", self.heater_entity_id) self._heater_turn_off() else: too_hot = self._cur_temp - self._target_temp >= \ self._hot_tolerance if too_hot: - _LOGGER.info('Turning on AC %s', self.heater_entity_id) + _LOGGER.info("Turning on AC %s", self.heater_entity_id) self._heater_turn_on() else: is_heating = self._is_device_active @@ -347,14 +365,14 @@ class GenericThermostat(ClimateDevice): too_hot = self._cur_temp - self._target_temp >= \ self._hot_tolerance if too_hot: - _LOGGER.info('Turning off heater %s', + _LOGGER.info("Turning off heater %s", self.heater_entity_id) self._heater_turn_off() else: too_cold = self._target_temp - self._cur_temp >= \ self._cold_tolerance if too_cold: - _LOGGER.info('Turning on heater %s', self.heater_entity_id) + _LOGGER.info("Turning on heater %s", self.heater_entity_id) self._heater_turn_on() @property @@ -365,7 +383,7 @@ class GenericThermostat(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @callback def _heater_turn_on(self): diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index 8305e772869..b8ac66d91b3 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -1,178 +1,178 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.hive/ -""" -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.components.hive import DATA_HIVE - -DEPENDENCIES = ['hive'] -HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, - 'ON': STATE_ON, 'OFF': STATE_OFF} -HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', - STATE_ON: 'ON', STATE_OFF: 'OFF'} - -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | - SUPPORT_OPERATION_MODE | - SUPPORT_AUX_HEAT) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Hive climate devices.""" - if discovery_info is None: - return - session = hass.data.get(DATA_HIVE) - - add_devices([HiveClimateEntity(session, discovery_info)]) - - -class HiveClimateEntity(ClimateDevice): - """Hive Climate Device.""" - - def __init__(self, hivesession, hivedevice): - """Initialize the Climate device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) - - if self.device_type == "Heating": - self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF] - elif self.device_type == "HotWater": - self.modes = [STATE_AUTO, STATE_ON, STATE_OFF] - - self.session.entities.append(self) - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - def handle_update(self, updatesource): - """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: - self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the Climate device.""" - friendly_name = "Climate Device" - if self.device_type == "Heating": - friendly_name = "Heating" - if self.node_name is not None: - friendly_name = '{} {}'.format(self.node_name, friendly_name) - elif self.device_type == "HotWater": - friendly_name = "Hot Water" - return friendly_name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if self.device_type == "Heating": - return self.session.heating.current_temperature(self.node_id) - - @property - def target_temperature(self): - """Return the target temperature.""" - if self.device_type == "Heating": - return self.session.heating.get_target_temperature(self.node_id) - - @property - def min_temp(self): - """Return minimum temperature.""" - if self.device_type == "Heating": - return self.session.heating.min_temperature(self.node_id) - - @property - def max_temp(self): - """Return the maximum temperature.""" - if self.device_type == "Heating": - return self.session.heating.max_temperature(self.node_id) - - @property - def operation_list(self): - """List of the operation modes.""" - return self.modes - - @property - def current_operation(self): - """Return current mode.""" - if self.device_type == "Heating": - currentmode = self.session.heating.get_mode(self.node_id) - elif self.device_type == "HotWater": - currentmode = self.session.hotwater.get_mode(self.node_id) - return HIVE_TO_HASS_STATE.get(currentmode) - - def set_operation_mode(self, operation_mode): - """Set new Heating mode.""" - new_mode = HASS_TO_HIVE_STATE.get(operation_mode) - if self.device_type == "Heating": - self.session.heating.set_mode(self.node_id, new_mode) - elif self.device_type == "HotWater": - self.session.hotwater.set_mode(self.node_id, new_mode) - - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - new_temperature = kwargs.get(ATTR_TEMPERATURE) - if new_temperature is not None: - if self.device_type == "Heating": - self.session.heating.set_target_temperature(self.node_id, - new_temperature) - - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - @property - def is_aux_heat_on(self): - """Return true if auxiliary heater is on.""" - boost_status = None - if self.device_type == "Heating": - boost_status = self.session.heating.get_boost(self.node_id) - elif self.device_type == "HotWater": - boost_status = self.session.hotwater.get_boost(self.node_id) - return boost_status == "ON" - - def turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - target_boost_time = 30 - if self.device_type == "Heating": - curtemp = self.session.heating.current_temperature(self.node_id) - curtemp = round(curtemp * 2) / 2 - target_boost_temperature = curtemp + 0.5 - self.session.heating.turn_boost_on(self.node_id, - target_boost_time, - target_boost_temperature) - elif self.device_type == "HotWater": - self.session.hotwater.turn_boost_on(self.node_id, - target_boost_time) - - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - if self.device_type == "Heating": - self.session.heating.turn_boost_off(self.node_id) - elif self.device_type == "HotWater": - self.session.hotwater.turn_boost_off(self.node_id) - - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def update(self): - """Update all Node data frome Hive.""" - self.session.core.update_data(self.node_id) +""" +Support for the Hive devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.hive/ +""" +from homeassistant.components.climate import ( + ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.hive import DATA_HIVE + +DEPENDENCIES = ['hive'] +HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, + 'ON': STATE_ON, 'OFF': STATE_OFF} +HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', + STATE_ON: 'ON', STATE_OFF: 'OFF'} + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | + SUPPORT_OPERATION_MODE | + SUPPORT_AUX_HEAT) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Hive climate devices.""" + if discovery_info is None: + return + session = hass.data.get(DATA_HIVE) + + add_devices([HiveClimateEntity(session, discovery_info)]) + + +class HiveClimateEntity(ClimateDevice): + """Hive Climate Device.""" + + def __init__(self, hivesession, hivedevice): + """Initialize the Climate device.""" + self.node_id = hivedevice["Hive_NodeID"] + self.node_name = hivedevice["Hive_NodeName"] + self.device_type = hivedevice["HA_DeviceType"] + self.session = hivesession + self.data_updatesource = '{}.{}'.format(self.device_type, + self.node_id) + + if self.device_type == "Heating": + self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF] + elif self.device_type == "HotWater": + self.modes = [STATE_AUTO, STATE_ON, STATE_OFF] + + self.session.entities.append(self) + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def handle_update(self, updatesource): + """Handle the new update request.""" + if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the Climate device.""" + friendly_name = "Climate Device" + if self.device_type == "Heating": + friendly_name = "Heating" + if self.node_name is not None: + friendly_name = '{} {}'.format(self.node_name, friendly_name) + elif self.device_type == "HotWater": + friendly_name = "Hot Water" + return friendly_name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + if self.device_type == "Heating": + return self.session.heating.current_temperature(self.node_id) + + @property + def target_temperature(self): + """Return the target temperature.""" + if self.device_type == "Heating": + return self.session.heating.get_target_temperature(self.node_id) + + @property + def min_temp(self): + """Return minimum temperature.""" + if self.device_type == "Heating": + return self.session.heating.min_temperature(self.node_id) + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.device_type == "Heating": + return self.session.heating.max_temperature(self.node_id) + + @property + def operation_list(self): + """List of the operation modes.""" + return self.modes + + @property + def current_operation(self): + """Return current mode.""" + if self.device_type == "Heating": + currentmode = self.session.heating.get_mode(self.node_id) + elif self.device_type == "HotWater": + currentmode = self.session.hotwater.get_mode(self.node_id) + return HIVE_TO_HASS_STATE.get(currentmode) + + def set_operation_mode(self, operation_mode): + """Set new Heating mode.""" + new_mode = HASS_TO_HIVE_STATE.get(operation_mode) + if self.device_type == "Heating": + self.session.heating.set_mode(self.node_id, new_mode) + elif self.device_type == "HotWater": + self.session.hotwater.set_mode(self.node_id, new_mode) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + new_temperature = kwargs.get(ATTR_TEMPERATURE) + if new_temperature is not None: + if self.device_type == "Heating": + self.session.heating.set_target_temperature(self.node_id, + new_temperature) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + @property + def is_aux_heat_on(self): + """Return true if auxiliary heater is on.""" + boost_status = None + if self.device_type == "Heating": + boost_status = self.session.heating.get_boost(self.node_id) + elif self.device_type == "HotWater": + boost_status = self.session.hotwater.get_boost(self.node_id) + return boost_status == "ON" + + def turn_aux_heat_on(self): + """Turn auxiliary heater on.""" + target_boost_time = 30 + if self.device_type == "Heating": + curtemp = self.session.heating.current_temperature(self.node_id) + curtemp = round(curtemp * 2) / 2 + target_boost_temperature = curtemp + 0.5 + self.session.heating.turn_boost_on(self.node_id, + target_boost_time, + target_boost_temperature) + elif self.device_type == "HotWater": + self.session.hotwater.turn_boost_on(self.node_id, + target_boost_time) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + def turn_aux_heat_off(self): + """Turn auxiliary heater off.""" + if self.device_type == "Heating": + self.session.heating.turn_boost_off(self.node_id) + elif self.device_type == "HotWater": + self.session.hotwater.turn_boost_off(self.node_id) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + def update(self): + """Update all Node data frome Hive.""" + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index 97bd3e9503c..a78c277fa33 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -5,13 +5,14 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.knx/ """ import asyncio + import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES from homeassistant.components.climate import ( - PLATFORM_SCHEMA, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_OPERATION_MODE) -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE + PLATFORM_SCHEMA, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + ClimateDevice) +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -61,24 +62,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up climate(s) for KNX platform.""" - if DATA_KNX not in hass.data \ - or not hass.data[DATA_KNX].initialized: - return False + if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized: + return if discovery_info is not None: async_add_devices_discovery(hass, discovery_info, async_add_devices) else: async_add_devices_config(hass, config, async_add_devices) - return True - @callback def async_add_devices_discovery(hass, discovery_info, async_add_devices): - """Set up climates for KNX platform configured within plattform.""" + """Set up climates for KNX platform configured within platform.""" entities = [] for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: device = hass.data[DATA_KNX].xknx.devices[device_name] @@ -88,28 +85,22 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices): @callback def async_add_devices_config(hass, config, async_add_devices): - """Set up climate for KNX platform configured within plattform.""" + """Set up climate for KNX platform configured within platform.""" import xknx climate = xknx.devices.Climate( hass.data[DATA_KNX].xknx, name=config.get(CONF_NAME), - group_address_temperature=config.get( - CONF_TEMPERATURE_ADDRESS), + group_address_temperature=config.get(CONF_TEMPERATURE_ADDRESS), group_address_target_temperature=config.get( CONF_TARGET_TEMPERATURE_ADDRESS), - group_address_setpoint_shift=config.get( - CONF_SETPOINT_SHIFT_ADDRESS), + group_address_setpoint_shift=config.get(CONF_SETPOINT_SHIFT_ADDRESS), group_address_setpoint_shift_state=config.get( CONF_SETPOINT_SHIFT_STATE_ADDRESS), - setpoint_shift_step=config.get( - CONF_SETPOINT_SHIFT_STEP), - setpoint_shift_max=config.get( - CONF_SETPOINT_SHIFT_MAX), - setpoint_shift_min=config.get( - CONF_SETPOINT_SHIFT_MIN), - group_address_operation_mode=config.get( - CONF_OPERATION_MODE_ADDRESS), + setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), + setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), + setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), + group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), group_address_operation_mode_state=config.get( CONF_OPERATION_MODE_STATE_ADDRESS), group_address_controller_status=config.get( @@ -127,10 +118,10 @@ def async_add_devices_config(hass, config, async_add_devices): class KNXClimate(ClimateDevice): - """Representation of a KNX climate.""" + """Representation of a KNX climate device.""" def __init__(self, hass, device): - """Initialization of KNXClimate.""" + """Initialize of a KNX climate device.""" self.device = device self.hass = hass self.async_register_callbacks() @@ -149,7 +140,7 @@ class KNXClimate(ClimateDevice): """Register callbacks to update hass after device was changed.""" @asyncio.coroutine def after_update_callback(device): - """Callback after device was updated.""" + """Call after device was updated.""" # pylint: disable=unused-argument yield from self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index ae71e5a48dc..3656bf7b475 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_AUX_HEAT) from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) + STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) @@ -35,21 +35,30 @@ DEFAULT_NAME = 'MQTT HVAC' CONF_POWER_COMMAND_TOPIC = 'power_command_topic' CONF_POWER_STATE_TOPIC = 'power_state_topic' +CONF_POWER_STATE_TEMPLATE = 'power_state_template' CONF_MODE_COMMAND_TOPIC = 'mode_command_topic' CONF_MODE_STATE_TOPIC = 'mode_state_topic' +CONF_MODE_STATE_TEMPLATE = 'mode_state_template' CONF_TEMPERATURE_COMMAND_TOPIC = 'temperature_command_topic' CONF_TEMPERATURE_STATE_TOPIC = 'temperature_state_topic' +CONF_TEMPERATURE_STATE_TEMPLATE = 'temperature_state_template' CONF_FAN_MODE_COMMAND_TOPIC = 'fan_mode_command_topic' CONF_FAN_MODE_STATE_TOPIC = 'fan_mode_state_topic' +CONF_FAN_MODE_STATE_TEMPLATE = 'fan_mode_state_template' CONF_SWING_MODE_COMMAND_TOPIC = 'swing_mode_command_topic' CONF_SWING_MODE_STATE_TOPIC = 'swing_mode_state_topic' +CONF_SWING_MODE_STATE_TEMPLATE = 'swing_mode_state_template' CONF_AWAY_MODE_COMMAND_TOPIC = 'away_mode_command_topic' CONF_AWAY_MODE_STATE_TOPIC = 'away_mode_state_topic' +CONF_AWAY_MODE_STATE_TEMPLATE = 'away_mode_state_template' CONF_HOLD_COMMAND_TOPIC = 'hold_command_topic' CONF_HOLD_STATE_TOPIC = 'hold_state_topic' +CONF_HOLD_STATE_TEMPLATE = 'hold_state_template' CONF_AUX_COMMAND_TOPIC = 'aux_command_topic' CONF_AUX_STATE_TOPIC = 'aux_state_topic' +CONF_AUX_STATE_TEMPLATE = 'aux_state_template' +CONF_CURRENT_TEMPERATURE_TEMPLATE = 'current_temperature_template' CONF_CURRENT_TEMPERATURE_TOPIC = 'current_temperature_topic' CONF_PAYLOAD_ON = 'payload_on' @@ -71,6 +80,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TEMPERATURE_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -79,6 +89,18 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMPERATURE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_TEMPERATURE_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_TEMPERATURE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_LIST, @@ -100,6 +122,26 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT climate devices.""" + template_keys = ( + CONF_POWER_STATE_TEMPLATE, + CONF_MODE_STATE_TEMPLATE, + CONF_TEMPERATURE_STATE_TEMPLATE, + CONF_FAN_MODE_STATE_TEMPLATE, + CONF_SWING_MODE_STATE_TEMPLATE, + CONF_AWAY_MODE_STATE_TEMPLATE, + CONF_HOLD_STATE_TEMPLATE, + CONF_AUX_STATE_TEMPLATE, + CONF_CURRENT_TEMPERATURE_TEMPLATE + ) + value_templates = {} + if CONF_VALUE_TEMPLATE in config: + value_template = config.get(CONF_VALUE_TEMPLATE) + value_template.hass = hass + value_templates = {key: value_template for key in template_keys} + for key in template_keys & config.keys(): + value_templates[key] = config.get(key) + value_templates[key].hass = hass + async_add_devices([ MqttClimate( hass, @@ -125,6 +167,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): CONF_CURRENT_TEMPERATURE_TOPIC ) }, + value_templates, config.get(CONF_QOS), config.get(CONF_RETAIN), config.get(CONF_MODE_LIST), @@ -145,18 +188,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttClimate(MqttAvailability, ClimateDevice): """Representation of a demo climate device.""" - def __init__(self, hass, name, topic, qos, retain, mode_list, - fan_mode_list, swing_mode_list, target_temperature, away, - hold, current_fan_mode, current_swing_mode, - current_operation, aux, send_if_off, payload_on, - payload_off, availability_topic, payload_available, - payload_not_available): + def __init__(self, hass, name, topic, value_templates, qos, retain, + mode_list, fan_mode_list, swing_mode_list, + target_temperature, away, hold, current_fan_mode, + current_swing_mode, current_operation, aux, send_if_off, + payload_on, payload_off, availability_topic, + payload_available, payload_not_available): """Initialize the climate device.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) self.hass = hass self._name = name self._topic = topic + self._value_templates = value_templates self._qos = qos self._retain = retain self._target_temperature = target_temperature @@ -184,6 +228,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_current_temp_received(topic, payload, qos): """Handle current temperature coming via MQTT.""" + if CONF_CURRENT_TEMPERATURE_TEMPLATE in self._value_templates: + payload =\ + self._value_templates[CONF_CURRENT_TEMPERATURE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + try: self._current_temperature = float(payload) self.async_schedule_update_ha_state() @@ -198,6 +247,10 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_mode_received(topic, payload, qos): """Handle receiving mode via MQTT.""" + if CONF_MODE_STATE_TEMPLATE in self._value_templates: + payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + if payload not in self._operation_list: _LOGGER.error("Invalid mode: %s", payload) else: @@ -212,6 +265,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_temperature_received(topic, payload, qos): """Handle target temperature coming via MQTT.""" + if CONF_TEMPERATURE_STATE_TEMPLATE in self._value_templates: + payload = \ + self._value_templates[CONF_TEMPERATURE_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + try: self._target_temperature = float(payload) self.async_schedule_update_ha_state() @@ -226,6 +284,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_fan_mode_received(topic, payload, qos): """Handle receiving fan mode via MQTT.""" + if CONF_FAN_MODE_STATE_TEMPLATE in self._value_templates: + payload = \ + self._value_templates[CONF_FAN_MODE_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + if payload not in self._fan_list: _LOGGER.error("Invalid fan mode: %s", payload) else: @@ -240,6 +303,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_swing_mode_received(topic, payload, qos): """Handle receiving swing mode via MQTT.""" + if CONF_SWING_MODE_STATE_TEMPLATE in self._value_templates: + payload = \ + self._value_templates[CONF_SWING_MODE_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + if payload not in self._swing_list: _LOGGER.error("Invalid swing mode: %s", payload) else: @@ -254,6 +322,15 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_away_mode_received(topic, payload, qos): """Handle receiving away mode via MQTT.""" + if CONF_AWAY_MODE_STATE_TEMPLATE in self._value_templates: + payload = \ + self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + if payload == "True": + payload = self._payload_on + elif payload == "False": + payload = self._payload_off + if payload == self._payload_on: self._away = True elif payload == self._payload_off: @@ -271,6 +348,14 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_aux_mode_received(topic, payload, qos): """Handle receiving aux mode via MQTT.""" + if CONF_AUX_STATE_TEMPLATE in self._value_templates: + payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + if payload == "True": + payload = self._payload_on + elif payload == "False": + payload = self._payload_off + if payload == self._payload_on: self._aux = True elif payload == self._payload_off: @@ -288,6 +373,10 @@ class MqttClimate(MqttAvailability, ClimateDevice): @callback def handle_hold_mode_received(topic, payload, qos): """Handle receiving hold mode via MQTT.""" + if CONF_HOLD_STATE_TEMPLATE in self._value_templates: + payload = self._value_templates[CONF_HOLD_STATE_TEMPLATE].\ + async_render_with_possible_json_value(payload) + self._hold = payload self.async_schedule_update_ha_state() diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index db43a6d3be4..ff1400a8fae 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/climate.mysensors/ from homeassistant.components import mysensors from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, - STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE) + STATE_COOL, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT DICT_HA_TO_MYS = { @@ -31,7 +32,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the mysensors climate.""" + """Set up the MySensors climate.""" mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) @@ -52,8 +53,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement.""" - return (TEMP_CELSIUS - if self.gateway.metric else TEMP_FAHRENHEIT) + return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT @property def current_temperature(self): @@ -139,7 +139,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value) if self.gateway.optimistic: - # optimistically assume that device has changed state + # O + # ptimistically assume that device has changed state self._values[value_type] = value self.schedule_update_ha_state() @@ -149,7 +150,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan) if self.gateway.optimistic: - # optimistically assume that device has changed state + # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan self.schedule_update_ha_state() @@ -159,7 +160,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[operation_mode]) if self.gateway.optimistic: - # optimistically assume that device has changed state + # Optimistically assume that device has changed state self._values[self.value_type] = operation_mode self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 3b550c43368..b4492821b1f 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.nest import DATA_NEST from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, @@ -27,8 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1)), }) -STATE_ECO = 'eco' -STATE_HEAT_COOL = 'heat-cool' +NEST_MODE_HEAT_COOL = 'heat-cool' SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE | @@ -118,14 +117,14 @@ class NestThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: return self._mode - elif self._mode == STATE_HEAT_COOL: + elif self._mode == NEST_MODE_HEAT_COOL: return STATE_AUTO return STATE_UNKNOWN @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: return self._target_temperature return None @@ -136,7 +135,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[0]: # eco_temperature is always a low, high tuple return self._eco_temperature[0] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[0] return None @@ -147,7 +146,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[1]: # eco_temperature is always a low, high tuple return self._eco_temperature[1] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[1] return None @@ -160,7 +159,7 @@ class NestThermostat(ClimateDevice): """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) else: @@ -173,7 +172,7 @@ class NestThermostat(ClimateDevice): if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: device_mode = operation_mode elif operation_mode == STATE_AUTO: - device_mode = STATE_HEAT_COOL + device_mode = NEST_MODE_HEAT_COOL self.device.mode = device_mode @property diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 870e2db6b42..67113e7c48a 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -294,14 +294,14 @@ class SensiboClimate(ClimateDevice): self._id, 'swing', swing_mode, self._ac_states) @asyncio.coroutine - def async_on(self): + def async_turn_on(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( self._id, 'on', True, self._ac_states) @asyncio.coroutine - def async_off(self): + def async_turn_off(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): yield from self._client.async_set_ac_state_property( diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/climate/tesla.py index 6295b85a1b7..459d9c666fd 100644 --- a/homeassistant/components/climate/tesla.py +++ b/homeassistant/components/climate/tesla.py @@ -6,13 +6,13 @@ https://home-assistant.io/components/climate.tesla/ """ import logging -from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.climate import ( - ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_OPERATION_MODE) -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice + ENTITY_ID_FORMAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + ClimateDevice) +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN +from homeassistant.components.tesla import TeslaDevice from homeassistant.const import ( - TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE) + ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) @@ -60,7 +60,7 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): return OPERATION_LIST def update(self): - """Called by the Tesla device callback to update state.""" + """Call by the Tesla device callback to update state.""" _LOGGER.debug("Updating: %s", self._name) self.tesla_device.update() self._target_temperature = self.tesla_device.get_goal_temp() diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/climate/toon.py index 0ff9f129081..330801fc231 100644 --- a/homeassistant/components/climate/toon.py +++ b/homeassistant/components/climate/toon.py @@ -7,25 +7,25 @@ Eneco. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.toon/ """ -import homeassistant.components.toon as toon_main from homeassistant.components.climate import ( - ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO, - STATE_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) + ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_PERFORMANCE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +import homeassistant.components.toon as toon_main from homeassistant.const import TEMP_CELSIUS SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Toon thermostat.""" + """Set up the Toon climate device.""" add_devices([ThermostatDevice(hass)], True) class ThermostatDevice(ClimateDevice): - """Interface class for the toon module and HA.""" + """Representation of a Toon climate device.""" def __init__(self, hass): - """Initialize the device.""" + """Initialize the Toon climate device.""" self._name = 'Toon van Eneco' self.hass = hass self.thermos = hass.data[toon_main.TOON_HANDLE] @@ -47,12 +47,12 @@ class ThermostatDevice(ClimateDevice): @property def name(self): - """Name of this Thermostat.""" + """Return the name of this thermostat.""" return self._name @property def temperature_unit(self): - """The unit of measurement used by the platform.""" + """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS @property @@ -63,7 +63,7 @@ class ThermostatDevice(ClimateDevice): @property def operation_list(self): - """List of available operation modes.""" + """Return a list of available operation modes.""" return self._operation_list @property @@ -82,7 +82,7 @@ class ThermostatDevice(ClimateDevice): self.thermos.set_temp(temp) def set_operation_mode(self, operation_mode): - """Set new operation mode as toonlib requires it.""" + """Set new operation mode.""" toonlib_values = { STATE_PERFORMANCE: 'Comfort', STATE_HEAT: 'Home', diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py new file mode 100644 index 00000000000..92e5c71b6c5 --- /dev/null +++ b/homeassistant/components/climate/venstar.py @@ -0,0 +1,267 @@ +""" +Support for Venstar WiFi Thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.venstar/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.climate import ( + ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, + CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, + TEMP_FAHRENHEIT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['venstarcolortouch==0.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_FAN_STATE = 'fan_state' +ATTR_HVAC_STATE = 'hvac_state' + +DEFAULT_SSL = False + +VALID_FAN_STATES = [STATE_ON, STATE_AUTO] +VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=5): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_USERNAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Venstar thermostat.""" + import venstarcolortouch + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + host = config.get(CONF_HOST) + timeout = config.get(CONF_TIMEOUT) + + if config.get(CONF_SSL): + proto = 'https' + else: + proto = 'http' + + client = venstarcolortouch.VenstarColorTouch( + addr=host, timeout=timeout, user=username, password=password, + proto=proto) + + add_devices([VenstarThermostat(client)], True) + + +class VenstarThermostat(ClimateDevice): + """Representation of a Venstar thermostat.""" + + def __init__(self, client): + """Initialize the thermostat.""" + self._client = client + + def update(self): + """Update the data from the thermostat.""" + info_success = self._client.update_info() + sensor_success = self._client.update_sensors() + if not info_success or not sensor_success: + _LOGGER.error("Failed to update data") + + @property + def supported_features(self): + """Return the list of supported features.""" + features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | + SUPPORT_OPERATION_MODE) + + if self._client.mode == self._client.MODE_AUTO: + features |= (SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + + if self._client.hum_active == 1: + features |= SUPPORT_TARGET_HUMIDITY + + return features + + @property + def name(self): + """Return the name of the thermostat.""" + return self._client.name + + @property + def precision(self): + """Return the precision of the system. + + Venstar temperature values are passed back and forth in the + API as whole degrees C or F. + """ + return PRECISION_WHOLE + + @property + def temperature_unit(self): + """Return the unit of measurement, as defined by the API.""" + if self._client.tempunits == self._client.TEMPUNITS_F: + return TEMP_FAHRENHEIT + else: + return TEMP_CELSIUS + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return VALID_FAN_STATES + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return VALID_THERMOSTAT_MODES + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._client.get_indoor_temp() + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._client.get_indoor_humidity() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if self._client.mode == self._client.MODE_HEAT: + return STATE_HEAT + elif self._client.mode == self._client.MODE_COOL: + return STATE_COOL + elif self._client.mode == self._client.MODE_AUTO: + return STATE_AUTO + else: + return STATE_OFF + + @property + def current_fan_mode(self): + """Return the fan setting.""" + if self._client.fan == self._client.FAN_AUTO: + return STATE_AUTO + else: + return STATE_ON + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + return { + ATTR_FAN_STATE: self._client.fanstate, + ATTR_HVAC_STATE: self._client.state, + } + + @property + def target_temperature(self): + """Return the target temperature we try to reach.""" + if self._client.mode == self._client.MODE_HEAT: + return self._client.heattemp + elif self._client.mode == self._client.MODE_COOL: + return self._client.cooltemp + else: + return None + + @property + def target_temperature_low(self): + """Return the lower bound temp if auto mode is on.""" + if self._client.mode == self._client.MODE_AUTO: + return self._client.heattemp + else: + return None + + @property + def target_temperature_high(self): + """Return the upper bound temp if auto mode is on.""" + if self._client.mode == self._client.MODE_AUTO: + return self._client.cooltemp + else: + return None + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + return self._client.hum_setpoint + + @property + def min_humidity(self): + """Return the minimum humidity. Hardcoded to 0 in API.""" + return 0 + + @property + def max_humidity(self): + """Return the maximum humidity. Hardcoded to 60 in API.""" + return 60 + + def _set_operation_mode(self, operation_mode): + """Change the operation mode (internal).""" + if operation_mode == STATE_HEAT: + success = self._client.set_mode(self._client.MODE_HEAT) + elif operation_mode == STATE_COOL: + success = self._client.set_mode(self._client.MODE_COOL) + elif operation_mode == STATE_AUTO: + success = self._client.set_mode(self._client.MODE_AUTO) + else: + success = self._client.set_mode(self._client.MODE_OFF) + + if not success: + _LOGGER.error("Failed to change the operation mode") + return success + + def set_temperature(self, **kwargs): + """Set a new target temperature.""" + set_temp = True + operation_mode = kwargs.get(ATTR_OPERATION_MODE, self._client.mode) + temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temperature = kwargs.get(ATTR_TEMPERATURE) + + if operation_mode != self._client.mode: + set_temp = self._set_operation_mode(operation_mode) + + if set_temp: + if operation_mode == self._client.MODE_HEAT: + success = self._client.set_setpoints( + temperature, self._client.cooltemp) + elif operation_mode == self._client.MODE_COOL: + success = self._client.set_setpoints( + self._client.heattemp, temperature) + elif operation_mode == self._client.MODE_AUTO: + success = self._client.set_setpoints(temp_low, temp_high) + else: + _LOGGER.error("The thermostat is currently not in a mode " + "that supports target temperature") + + if not success: + _LOGGER.error("Failed to change the temperature") + + def set_fan_mode(self, fan): + """Set new target fan mode.""" + if fan == STATE_ON: + success = self._client.set_fan(self._client.FAN_ON) + else: + success = self._client.set_fan(self._client.FAN_AUTO) + + if not success: + _LOGGER.error("Failed to change the fan mode") + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + self._set_operation_mode(operation_mode) + + def set_humidity(self, humidity): + """Set new target humidity.""" + success = self._client.set_hum_setpoint(humidity) + + if not success: + _LOGGER.error("Failed to change the target humidity level") diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 33ba0f56d33..50374a32807 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -8,16 +8,16 @@ import asyncio import logging from homeassistant.components.climate import ( - STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC, - STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND, - STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY, - ATTR_TARGET_TEMP_HIGH, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, + ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_ELECTRIC, + STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, STATE_HIGH_DEMAND, + STATE_PERFORMANCE, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, - SUPPORT_AUX_HEAT) + ClimateDevice) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import ( - STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS) + PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS) from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,8 @@ ATTR_SCHEDULE_ENABLED = 'schedule_enabled' ATTR_SMART_TEMPERATURE = 'smart_temperature' ATTR_TOTAL_CONSUMPTION = 'total_consumption' ATTR_VACATION_MODE = 'vacation_mode' +ATTR_HEAT_ON = 'heat_on' +ATTR_COOL_ON = 'cool_on' DEPENDENCIES = ['wink'] @@ -93,7 +95,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['climate'].append(self) @property @@ -104,7 +106,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def device_state_attributes(self): - """Return the optional state attributes.""" + """Return the optional device state attributes.""" data = {} target_temp_high = self.target_temperature_high target_temp_low = self.target_temperature_low @@ -131,6 +133,12 @@ class WinkThermostat(WinkDevice, ClimateDevice): if self.eco_target: data[ATTR_ECO_TARGET] = self.eco_target + if self.heat_on: + data[ATTR_HEAT_ON] = self.heat_on + + if self.cool_on: + data[ATTR_COOL_ON] = self.cool_on + current_humidity = self.current_humidity if current_humidity is not None: data[ATTR_CURRENT_HUMIDITY] = current_humidity @@ -174,6 +182,16 @@ class WinkThermostat(WinkDevice, ClimateDevice): """Return status of if the thermostat has detected occupancy.""" return self.wink.occupied() + @property + def heat_on(self): + """Return whether or not the heat is actually heating.""" + return self.wink.heat_on() + + @property + def cool_on(self): + """Return whether or not the heat is actually heating.""" + return self.wink.heat_on() + @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" @@ -385,7 +403,7 @@ class WinkAC(WinkDevice, ClimateDevice): @property def device_state_attributes(self): - """Return the optional state attributes.""" + """Return the optional device state attributes.""" data = {} target_temp_high = self.target_temperature_high target_temp_low = self.target_temperature_low @@ -508,7 +526,7 @@ class WinkWaterHeater(WinkDevice, ClimateDevice): @property def device_state_attributes(self): - """Return the optional state attributes.""" + """Return the optional device state attributes.""" data = {} data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() data[ATTR_RHEEM_TYPE] = self.wink.rheem_type() diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index e497f4677e4..a5bbf805d42 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,4 +1,9 @@ -"""Component to integrate the Home Assistant cloud.""" +""" +Component to integrate the Home Assistant cloud. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/cloud/ +""" import asyncio from datetime import datetime import json @@ -26,18 +31,18 @@ REQUIREMENTS = ['warrant==0.6.1'] _LOGGER = logging.getLogger(__name__) CONF_ALEXA = 'alexa' -CONF_GOOGLE_ACTIONS = 'google_actions' -CONF_FILTER = 'filter' +CONF_ALIASES = 'aliases' CONF_COGNITO_CLIENT_ID = 'cognito_client_id' +CONF_ENTITY_CONFIG = 'entity_config' +CONF_FILTER = 'filter' +CONF_GOOGLE_ACTIONS = 'google_actions' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' -CONF_ALIASES = 'aliases' -MODE_DEV = 'development' DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] -CONF_ENTITY_CONFIG = 'entity_config' +MODE_DEV = 'development' ALEXA_ENTITY_SCHEMA = vol.Schema({ vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string, @@ -149,7 +154,7 @@ class Cloud: @property def subscription_expired(self): - """Return a boolen if the subscription has expired.""" + """Return a boolean if the subscription has expired.""" return dt_util.utcnow() > self.expiration_date @property @@ -195,8 +200,8 @@ class Cloud: if not jwt_success: return False - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, - self._start_cloud) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._start_cloud) return True @@ -248,7 +253,7 @@ class Cloud: for token in 'id_token', 'access_token': self._decode_claims(info[token]) except ValueError as err: # Raised when token is invalid - _LOGGER.warning('Found invalid token %s: %s', token, err) + _LOGGER.warning("Found invalid token %s: %s", token, err) return self.id_token = info['id_token'] @@ -282,15 +287,15 @@ class Cloud: header = jwt.get_unverified_header(token) except jose_exceptions.JWTError as err: raise ValueError(str(err)) from None - kid = header.get("kid") + kid = header.get('kid') if kid is None: - raise ValueError('No kid in header') + raise ValueError("No kid in header") # Locate the key for this kid key = None - for key_dict in self.jwt_keyset["keys"]: - if key_dict["kid"] == kid: + for key_dict in self.jwt_keyset['keys']: + if key_dict['kid'] == kid: key = key_dict break if not key: diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py index 500ff062a0f..e96f2a2d8a5 100644 --- a/homeassistant/components/cloud/auth_api.py +++ b/homeassistant/components/cloud/auth_api.py @@ -1,7 +1,6 @@ """Package to communicate with the authentication API.""" import logging - _LOGGER = logging.getLogger(__name__) @@ -22,7 +21,7 @@ class UserNotConfirmed(CloudError): class ExpiredCode(CloudError): - """Raised when an expired code is encoutered.""" + """Raised when an expired code is encountered.""" class InvalidCode(CloudError): @@ -38,7 +37,7 @@ class PasswordChangeRequired(CloudError): class UnknownError(CloudError): - """Raised when an unknown error occurrs.""" + """Raised when an unknown error occurs.""" AWS_EXCEPTIONS = { @@ -98,7 +97,7 @@ def resend_email_confirm(cloud, email): def forgot_password(cloud, email): - """Initiate forgotten password flow.""" + """Initialize forgotten password flow.""" from botocore.exceptions import ClientError cognito = _cognito(cloud, username=email) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 338e004ce52..af966e180eb 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -3,8 +3,8 @@ import asyncio from functools import wraps import logging -import voluptuous as vol import async_timeout +import voluptuous as vol from homeassistant.components.http import ( HomeAssistantView, RequestDataValidator) @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup(hass): - """Initialize the HTTP api.""" + """Initialize the HTTP API.""" hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudAccountView) @@ -40,7 +40,7 @@ _CLOUD_ERRORS = { def _handle_cloud_errors(handler): - """Helper method to handle auth errors.""" + """Handle auth errors.""" @asyncio.coroutine @wraps(handler) def error_handler(view, request, *args, **kwargs): diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index ffe68c3c877..2d3ab025e43 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -12,7 +12,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import auth_api from .const import MESSAGE_EXPIRATION - HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) @@ -85,7 +84,7 @@ class CloudIoT: }) self.tries = 0 - _LOGGER.info('Connected') + _LOGGER.info("Connected") self.state = STATE_CONNECTED while not client.closed: @@ -107,7 +106,7 @@ class CloudIoT: disconnect_warn = 'Received invalid JSON.' break - _LOGGER.debug('Received message: %s', msg) + _LOGGER.debug("Received message: %s", msg) response = { 'msgid': msg['msgid'], @@ -126,14 +125,14 @@ class CloudIoT: response['error'] = 'unknown-handler' except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error handling message') + _LOGGER.exception("Error handling message") response['error'] = 'exception' - _LOGGER.debug('Publishing message: %s', response) + _LOGGER.debug("Publishing message: %s", response) yield from client.send_json(response) except auth_api.CloudError: - _LOGGER.warning('Unable to connect: Unable to refresh token.') + _LOGGER.warning("Unable to connect: Unable to refresh token.") except client_exceptions.WSServerHandshakeError as err: if err.code == 401: @@ -141,18 +140,18 @@ class CloudIoT: self.close_requested = True # Should we notify user? else: - _LOGGER.warning('Unable to connect: %s', err) + _LOGGER.warning("Unable to connect: %s", err) except client_exceptions.ClientError as err: - _LOGGER.warning('Unable to connect: %s', err) + _LOGGER.warning("Unable to connect: %s", err) except Exception: # pylint: disable=broad-except if not self.close_requested: - _LOGGER.exception('Unexpected error') + _LOGGER.exception("Unexpected error") finally: if disconnect_warn is not None: - _LOGGER.warning('Connection closed: %s', disconnect_warn) + _LOGGER.warning("Connection closed: %s", disconnect_warn) if remove_hass_stop_listener is not None: remove_hass_stop_listener() @@ -169,7 +168,7 @@ class CloudIoT: self.tries += 1 try: - # Sleep 0, 5, 10, 15 … up to 30 seconds between retries + # Sleep 0, 5, 10, 15 ... up to 30 seconds between retries self.retry_task = hass.async_add_job(asyncio.sleep( min(30, (self.tries - 1) * 5), loop=hass.loop)) yield from self.retry_task @@ -205,8 +204,8 @@ def async_handle_message(hass, cloud, handler_name, payload): @asyncio.coroutine def async_handle_alexa(hass, cloud, payload): """Handle an incoming IoT message for Alexa.""" - result = yield from alexa.async_handle_message(hass, cloud.alexa_config, - payload) + result = yield from alexa.async_handle_message( + hass, cloud.alexa_config, payload) return result @@ -214,8 +213,8 @@ def async_handle_alexa(hass, cloud, payload): @asyncio.coroutine def async_handle_google_actions(hass, cloud, payload): """Handle an incoming IoT message for Google Actions.""" - result = yield from ga.async_handle_message(hass, cloud.gactions_config, - payload) + result = yield from ga.async_handle_message( + hass, cloud.gactions_config, payload) return result @@ -227,9 +226,9 @@ def async_handle_cloud(hass, cloud, payload): if action == 'logout': yield from cloud.logout() - _LOGGER.error('You have been logged out from Home Assistant cloud: %s', + _LOGGER.error("You have been logged out from Home Assistant cloud: %s", payload['reason']) else: - _LOGGER.warning('Received unknown cloud action: %s', action) + _LOGGER.warning("Received unknown cloud action: %s", action) return None diff --git a/homeassistant/components/comfoconnect.py b/homeassistant/components/comfoconnect.py index ba2180078e3..425ed6f9c9a 100644 --- a/homeassistant/components/comfoconnect.py +++ b/homeassistant/components/comfoconnect.py @@ -8,11 +8,11 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import (discovery) -from homeassistant.helpers.dispatcher import (dispatcher_send) + CONF_HOST, CONF_NAME, CONF_PIN, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send REQUIREMENTS = ['pycomfoconnect==0.3'] @@ -115,7 +115,7 @@ class ComfoConnectBridge(object): self.comfoconnect.disconnect() def sensor_callback(self, var, value): - """Callback function for sensor updates.""" + """Call function for sensor updates.""" _LOGGER.debug("Got value from bridge: %d = %d", var, value) from pycomfoconnect import ( diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index 9e3d675cabe..1fa215e5fb9 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -5,9 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.homematic/ """ import logging -from homeassistant.const import STATE_UNKNOWN -from homeassistant.components.cover import CoverDevice, ATTR_POSITION + +from homeassistant.components.cover import CoverDevice, ATTR_POSITION,\ + ATTR_TILT_POSITION from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN _LOGGER = logging.getLogger(__name__) @@ -69,3 +71,40 @@ class HMCover(HMDevice, CoverDevice): """Generate a data dictoinary (self._data) from metadata.""" self._state = "LEVEL" self._data.update({self._state: STATE_UNKNOWN}) + if "LEVEL_2" in self._hmdevice.WRITENODE: + self._data.update( + {'LEVEL_2': STATE_UNKNOWN}) + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + if 'LEVEL_2' not in self._data: + return None + + return int(self._data.get('LEVEL_2', 0) * 100) + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if "LEVEL_2" in self._data and ATTR_TILT_POSITION in kwargs: + position = float(kwargs[ATTR_TILT_POSITION]) + position = min(100, max(0, position)) + level = position / 100.0 + self._hmdevice.set_cover_tilt_position(level, self._channel) + + def open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + if "LEVEL_2" in self._data: + self._hmdevice.open_slats() + + def close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + if "LEVEL_2" in self._data: + self._hmdevice.close_slats() + + def stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + if "LEVEL_2" in self._data: + self.stop_cover(**kwargs) diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index d8313caeb5f..79c57c41e90 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -5,17 +5,18 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.knx/ """ import asyncio + import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES -from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, - SUPPORT_SET_POSITION, SUPPORT_STOP, SUPPORT_SET_TILT_POSITION, - ATTR_POSITION, ATTR_TILT_POSITION) -from homeassistant.core import callback + ATTR_POSITION, ATTR_TILT_POSITION, PLATFORM_SCHEMA, SUPPORT_CLOSE, + SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverDevice) +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.const import CONF_NAME +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_utc_time_change CONF_MOVE_LONG_ADDRESS = 'move_long_address' CONF_MOVE_SHORT_ADDRESS = 'move_short_address' @@ -50,20 +51,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up cover(s) for KNX platform.""" - if DATA_KNX not in hass.data \ - or not hass.data[DATA_KNX].initialized: - return False + if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized: + return if discovery_info is not None: async_add_devices_discovery(hass, discovery_info, async_add_devices) else: async_add_devices_config(hass, config, async_add_devices) - return True - @callback def async_add_devices_discovery(hass, discovery_info, async_add_devices): @@ -114,7 +111,7 @@ class KNXCover(CoverDevice): """Register callbacks to update hass after device was changed.""" @asyncio.coroutine def after_update_callback(device): - """Callback after device was updated.""" + """Call after device was updated.""" # pylint: disable=unused-argument yield from self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) @@ -209,7 +206,7 @@ class KNXCover(CoverDevice): @callback def auto_updater_hook(self, now): - """Callback for autoupdater.""" + """Call for the autoupdater.""" # pylint: disable=unused-argument self.async_schedule_update_ha_state() if self.device.position_reached(): diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py new file mode 100644 index 00000000000..08a2ef8c5ad --- /dev/null +++ b/homeassistant/components/cover/lutron.py @@ -0,0 +1,76 @@ +""" +Support for Lutron shades. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.lutron/ +""" +import logging + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, + ATTR_POSITION) +from homeassistant.components.lutron import ( + LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Lutron shades.""" + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]['cover']: + dev = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_devices(devs, True) + return True + + +class LutronCover(LutronDevice, CoverDevice): + """Representation of a Lutron shade.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._lutron_device.last_level() < 1 + + @property + def current_cover_position(self): + """Return the current position of cover.""" + return self._lutron_device.last_level() + + def close_cover(self, **kwargs): + """Close the cover.""" + self._lutron_device.level = 0 + + def open_cover(self, **kwargs): + """Open the cover.""" + self._lutron_device.level = 100 + + def set_cover_position(self, **kwargs): + """Move the shade to a specific position.""" + if ATTR_POSITION in kwargs: + position = kwargs[ATTR_POSITION] + self._lutron_device.level = position + + def update(self): + """Call when forcing a refresh of the device.""" + # Reading the property (rather than last_level()) fetchs value + level = self._lutron_device.level + _LOGGER.debug("Lutron ID: %d updated to %f", + self._lutron_device.id, level) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr['Lutron Integration ID'] = self._lutron_device.id + return attr diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index cd4ff62b3e9..391d2a22bda 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -5,12 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mysensors/ """ from homeassistant.components import mysensors -from homeassistant.components.cover import CoverDevice, ATTR_POSITION, DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice +from homeassistant.const import STATE_OFF, STATE_ON def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the mysensors platform for covers.""" + """Set up the MySensors platform for covers.""" mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices) diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index 66f2fde52f4..aefb7ab89d7 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -29,12 +29,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the RFXtrx cover.""" import RFXtrx as rfxtrxmod covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) - add_devices_callback(covers) + add_devices(covers) def cover_update(event): """Handle cover updates from the RFXtrx gateway.""" @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) if new_device: - add_devices_callback([new_device]) + add_devices([new_device]) rfxtrx.apply_received_command(event) @@ -59,7 +59,7 @@ class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): @property def should_poll(self): - """No polling available in RFXtrx cover.""" + """Return the polling state. No polling available in RFXtrx cover.""" return False @property diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 9968e3d6503..fd2b5847292 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/cover.tahoma/ import logging from datetime import timedelta -from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT +from homeassistant.components.cover import CoverDevice from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) @@ -30,11 +30,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TahomaCover(TahomaDevice, CoverDevice): """Representation a Tahoma Cover.""" - def __init__(self, tahoma_device, controller): - """Initialize the Tahoma device.""" - super().__init__(tahoma_device, controller) - self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) - def update(self): """Update method.""" self.controller.get_states([self.tahoma_device]) @@ -46,12 +41,16 @@ class TahomaCover(TahomaDevice, CoverDevice): 0 is closed, 100 is fully open. """ - position = 100 - self.tahoma_device.active_states['core:ClosureState'] - if position <= 5: - return 0 - if position >= 95: - return 100 - return position + try: + position = 100 - \ + self.tahoma_device.active_states['core:ClosureState'] + if position <= 5: + return 0 + if position >= 95: + return 100 + return position + except KeyError: + return None def set_cover_position(self, position, **kwargs): """Move the cover to a specific position.""" @@ -63,6 +62,14 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.current_cover_position is not None: return self.current_cover_position == 0 + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent': + return 'window' + else: + return None + def open_cover(self, **kwargs): """Open the cover.""" self.apply_action('open') @@ -78,10 +85,3 @@ class TahomaCover(TahomaDevice, CoverDevice): self.apply_action('setPosition', 'secured') else: self.apply_action('stopIdentify') - - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent': - return 'window' - else: - return None diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index ce96b4d75e0..35f14e80b5b 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/cover.wink/ """ import asyncio -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN from homeassistant.components.wink import WinkDevice, DOMAIN DEPENDENCIES = ['wink'] @@ -31,25 +31,28 @@ class WinkCoverDevice(WinkDevice, CoverDevice): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['cover'].append(self) def close_cover(self, **kwargs): - """Close the shade.""" + """Close the cover.""" self.wink.set_state(0) def open_cover(self, **kwargs): - """Open the shade.""" + """Open the cover.""" self.wink.set_state(1) def set_cover_position(self, position, **kwargs): - """Move the roller shutter to a specific position.""" + """Move the cover shutter to a specific position.""" self.wink.set_state(float(position)/100) @property def current_cover_position(self): - """Return the current position of roller shutter.""" - return int(self.wink.state()*100) + """Return the current position of cover shutter.""" + if self.wink.state() is not None: + return int(self.wink.state()*100) + else: + return STATE_UNKNOWN @property def is_closed(self): diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/cover/xiaomi_aqara.py index 5b51371346b..29cb707fef5 100644 --- a/homeassistant/components/cover/xiaomi_aqara.py +++ b/homeassistant/components/cover/xiaomi_aqara.py @@ -59,7 +59,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice): """Move the cover to a specific position.""" self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)}) - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if ATTR_CURTAIN_LEVEL in data: self._pos = int(data[ATTR_CURTAIN_LEVEL]) diff --git a/homeassistant/components/datadog.py b/homeassistant/components/datadog.py index 2c8145177b7..58503d7187b 100644 --- a/homeassistant/components/datadog.py +++ b/homeassistant/components/datadog.py @@ -5,11 +5,12 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/datadog/ """ import logging + import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX, - EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, - STATE_UNKNOWN) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv @@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Setup the Datadog component.""" + """Set up the Datadog component.""" from datadog import initialize, statsd conf = config[DOMAIN] @@ -81,36 +82,19 @@ def setup(hass, config): if isinstance(value, (float, int)): attribute = "{}.{}".format(metric, key.replace(' ', '_')) statsd.gauge( - attribute, - value, - sample_rate=sample_rate, - tags=tags - ) + attribute, value, sample_rate=sample_rate, tags=tags) _LOGGER.debug( - 'Sent metric %s: %s (tags: %s)', - attribute, - value, - tags - ) + "Sent metric %s: %s (tags: %s)", attribute, value, tags) try: value = state_helper.state_as_number(state) except ValueError: _LOGGER.debug( - 'Error sending %s: %s (tags: %s)', - metric, - state.state, - tags - ) + "Error sending %s: %s (tags: %s)", metric, state.state, tags) return - statsd.gauge( - metric, - value, - sample_rate=sample_rate, - tags=tags - ) + statsd.gauge(metric, value, sample_rate=sample_rate, tags=tags) _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 021febdc07c..269b8136020 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,20 +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 asyncio import logging + import voluptuous as vol +from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==23'] +REQUIREMENTS = ['pydeconz==25'] _LOGGER = logging.getLogger(__name__) @@ -27,8 +27,8 @@ CONFIG_FILE = 'deconz.conf' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=80): cv.port, }) }, extra=vol.ALLOW_EXTRA) @@ -53,14 +53,14 @@ Unlock your deCONZ gateway to register with Home Assistant. @asyncio.coroutine def async_setup(hass, config): - """Setup services and configuration for deCONZ component.""" + """Set up services and configuration for deCONZ component.""" result = False config_file = yield from hass.async_add_job( load_json, hass.config.path(CONFIG_FILE)) @asyncio.coroutine def async_deconz_discovered(service, discovery_info): - """Called when deCONZ gateway has been found.""" + """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) @@ -85,17 +85,18 @@ def async_setup(hass, config): @asyncio.coroutine def async_setup_deconz(hass, config, deconz_config): - """Setup deCONZ session. + """Set up a deCONZ session. Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ + _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession websession = async_get_clientsession(hass) deconz = DeconzSession(hass.loop, websession, **deconz_config) result = yield from deconz.async_load_parameters() if result is False: - _LOGGER.error("Failed to communicate with deCONZ.") + _LOGGER.error("Failed to communicate with deCONZ") return False hass.data[DOMAIN] = deconz @@ -125,8 +126,7 @@ def async_setup_deconz(hass, config, deconz_config): data = call.data.get(SERVICE_DATA) yield from deconz.async_put_state(field, data) hass.services.async_register( - DOMAIN, 'configure', async_configure, - schema=SERVICE_SCHEMA) + DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close) return True @@ -146,9 +146,8 @@ def async_request_configuration(hass, config, deconz_config): deconz_config[CONF_API_KEY] = api_key result = yield from async_setup_deconz(hass, config, deconz_config) if result: - yield from hass.async_add_job(save_json, - hass.config.path(CONFIG_FILE), - deconz_config) + yield from hass.async_add_job( + save_json, hass.config.path(CONFIG_FILE), deconz_config) configurator.async_request_done(request_id) return else: diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index f49f54b3622..0d27c4b5efd 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUB_KEY = 'pub_key' CONF_SSH_KEY = 'ssh_key' - DEFAULT_SSH_PORT = 22 - SECRET_GROUP = 'Password or SSH Key' PLATFORM_SCHEMA = vol.All( @@ -118,20 +116,10 @@ class AsusWrtDeviceScanner(DeviceScanner): self.port = config[CONF_PORT] if self.protocol == 'ssh': - if not (self.ssh_key or self.password): - _LOGGER.error("No password or private key specified") - self.success_init = False - return - self.connection = SshConnection( self.host, self.port, self.username, self.password, self.ssh_key, self.mode == 'ap') else: - if not self.password: - _LOGGER.error("No password specified") - self.success_init = False - return - self.connection = TelnetConnection( self.host, self.port, self.username, self.password, self.mode == 'ap') @@ -177,11 +165,16 @@ class AsusWrtDeviceScanner(DeviceScanner): """ devices = {} devices.update(self._get_wl()) - devices = self._get_arp(devices) - devices = self._get_neigh(devices) + devices.update(self._get_arp()) + devices.update(self._get_neigh(devices)) if not self.mode == 'ap': devices.update(self._get_leases(devices)) - return devices + + ret_devices = {} + for key in devices: + if devices[key].ip is not None: + ret_devices[key] = devices[key] + return ret_devices def _get_wl(self): lines = self.connection.run_command(_WL_CMD) @@ -219,18 +212,13 @@ class AsusWrtDeviceScanner(DeviceScanner): result = _parse_lines(lines, _IP_NEIGH_REGEX) devices = {} for device in result: - if device['mac']: + if device['mac'] is not None: mac = device['mac'].upper() - devices[mac] = Device(mac, None, None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + old_ip = cur_devices.get(mac, {}).ip or None + devices[mac] = Device(mac, device.get('ip', old_ip), None) + return devices - def _get_arp(self, cur_devices): + def _get_arp(self): lines = self.connection.run_command(_ARP_CMD) if not lines: return {} @@ -240,13 +228,7 @@ class AsusWrtDeviceScanner(DeviceScanner): if device['mac']: mac = device['mac'].upper() devices[mac] = Device(mac, device['ip'], None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + return devices class _Connection: @@ -272,7 +254,7 @@ class SshConnection(_Connection): def __init__(self, host, port, username, password, ssh_key, ap): """Initialize the SSH connection properties.""" - super(SshConnection, self).__init__() + super().__init__() self._ssh = None self._host = host @@ -322,7 +304,7 @@ class SshConnection(_Connection): self._ssh.login(self._host, self._username, password=self._password, port=self._port) - super(SshConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -334,7 +316,7 @@ class SshConnection(_Connection): finally: self._ssh = None - super(SshConnection, self).disconnect() + super().disconnect() class TelnetConnection(_Connection): @@ -342,7 +324,7 @@ class TelnetConnection(_Connection): def __init__(self, host, port, username, password, ap): """Initialize the Telnet connection properties.""" - super(TelnetConnection, self).__init__() + super().__init__() self._telnet = None self._host = host @@ -361,7 +343,6 @@ class TelnetConnection(_Connection): try: if not self.connected: self.connect() - self._telnet.write('{}\n'.format(command).encode('ascii')) data = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) @@ -392,7 +373,7 @@ class TelnetConnection(_Connection): self._telnet.write((self._password + '\n').encode('ascii')) self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1] - super(TelnetConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -402,4 +383,4 @@ class TelnetConnection(_Connection): except Exception: pass - super(TelnetConnection, self).disconnect() + super().disconnect() diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index ef747657cb4..5ad3995ad2a 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -14,8 +14,8 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_MAC, - ATTR_GPS, ATTR_GPS_ACCURACY) + ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_HOST_NAME, + ATTR_MAC, PLATFORM_SCHEMA) from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -24,35 +24,33 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval REQUIREMENTS = ['aioautomatic==0.6.4'] -DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = 'client_id' -CONF_SECRET = 'secret' -CONF_DEVICES = 'devices' -CONF_CURRENT_LOCATION = 'current_location' - -DEFAULT_TIMEOUT = 5 - -DEFAULT_SCOPE = ['location', 'trip', 'vehicle:events', 'vehicle:profile'] -FULL_SCOPE = DEFAULT_SCOPE + ['current_location'] - ATTR_FUEL_LEVEL = 'fuel_level' - -EVENT_AUTOMATIC_UPDATE = 'automatic_update' - AUTOMATIC_CONFIG_FILE = '.automatic/session-{}.json' +CONF_CLIENT_ID = 'client_id' +CONF_CURRENT_LOCATION = 'current_location' +CONF_DEVICES = 'devices' +CONF_SECRET = 'secret' + DATA_CONFIGURING = 'automatic_configurator_clients' DATA_REFRESH_TOKEN = 'refresh_token' +DEFAULT_SCOPE = ['location', 'trip', 'vehicle:events', 'vehicle:profile'] +DEFAULT_TIMEOUT = 5 +DEPENDENCIES = ['http'] + +EVENT_AUTOMATIC_UPDATE = 'automatic_update' + +FULL_SCOPE = DEFAULT_SCOPE + ['current_location'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_SECRET): cv.string, vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, - vol.Optional(CONF_DEVICES, default=None): vol.All( - cv.ensure_list, [cv.string]) + vol.Optional(CONF_DEVICES, default=None): + vol.All(cv.ensure_list, [cv.string]), }) @@ -142,7 +140,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): @asyncio.coroutine def initialize_callback(code, state): - """Callback after OAuth2 response is returned.""" + """Call after OAuth2 response is returned.""" try: session = yield from client.create_session_from_oauth_code( code, state) @@ -181,12 +179,12 @@ class AutomaticAuthCallbackView(HomeAssistantView): return response else: _LOGGER.error( - "Error authorizing Automatic. Invalid response returned.") + "Error authorizing Automatic. Invalid response returned") return response if DATA_CONFIGURING not in hass.data or \ params['state'] not in hass.data[DATA_CONFIGURING]: - _LOGGER.error("Automatic configuration request not found.") + _LOGGER.error("Automatic configuration request not found") return response code = params['code'] @@ -220,16 +218,15 @@ class AutomaticData(object): @asyncio.coroutine def handle_event(self, name, event): - """Coroutine to update state for a realtime event.""" + """Coroutine to update state for a real time event.""" import aioautomatic - # Fire a hass event self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) if event.vehicle.id not in self.vehicle_info: # If vehicle hasn't been seen yet, request the detailed # info for this vehicle. - _LOGGER.info("New vehicle found.") + _LOGGER.info("New vehicle found") try: vehicle = yield from event.get_vehicle() except aioautomatic.exceptions.AutomaticError as err: @@ -240,7 +237,7 @@ class AutomaticData(object): if event.created_at < self.vehicle_seen[event.vehicle.id]: # Skip events received out of order _LOGGER.debug("Skipping out of order event. Event Created %s. " - "Last seen event: %s.", event.created_at, + "Last seen event: %s", event.created_at, self.vehicle_seen[event.vehicle.id]) return self.vehicle_seen[event.vehicle.id] = event.created_at @@ -270,13 +267,13 @@ class AutomaticData(object): self.ws_close_requested = False if self.ws_reconnect_handle is not None: - _LOGGER.debug("Retrying websocket connection.") + _LOGGER.debug("Retrying websocket connection") try: ws_loop_future = yield from self.client.ws_connect() except aioautomatic.exceptions.UnauthorizedClientError: _LOGGER.error("Client unauthorized for websocket connection. " "Ensure Websocket is selected in the Automatic " - "developer application event delivery preferences.") + "developer application event delivery preferences") return except aioautomatic.exceptions.AutomaticError as err: if self.ws_reconnect_handle is None: @@ -290,14 +287,14 @@ class AutomaticData(object): self.ws_reconnect_handle() self.ws_reconnect_handle = None - _LOGGER.info("Websocket connected.") + _LOGGER.info("Websocket connected") try: yield from ws_loop_future except aioautomatic.exceptions.AutomaticError as err: _LOGGER.error(str(err)) - _LOGGER.info("Websocket closed.") + _LOGGER.info("Websocket closed") # If websocket was close was not requested, attempt to reconnect if not self.ws_close_requested: diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 32d677a59db..1742a0aed95 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -15,7 +15,10 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components import zone as zone_comp -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_GPS +) from homeassistant.const import STATE_HOME from homeassistant.core import callback from homeassistant.util import slugify, decorator @@ -140,6 +143,11 @@ def _parse_see_args(message, subscribe_topic): kwargs['attributes']['tid'] = message['tid'] if 'addr' in message: kwargs['attributes']['address'] = message['addr'] + if 't' in message: + if message['t'] == 'c': + kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS + if message['t'] == 'b': + kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_BLUETOOTH_LE return dev_id, kwargs diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 57e83eaeb94..7cebf0abdf4 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -14,7 +14,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + CONF_PASSWORD, CONF_USERNAME) CONF_HTTP_ID = 'http_id' @@ -22,6 +24,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=-1): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): vol.Any( + cv.boolean, cv.isfile), vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_HTTP_ID): cv.string @@ -39,16 +45,23 @@ class TomatoDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" host, http_id = config[CONF_HOST], config[CONF_HTTP_ID] + port = config[CONF_PORT] username, password = config[CONF_USERNAME], config[CONF_PASSWORD] + self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL] + if port == -1: + port = 80 + if self.ssl: + port = 443 self.req = requests.Request( - 'POST', 'http://{}/update.cgi'.format(host), + 'POST', 'http{}://{}:{}/update.cgi'.format( + "s" if self.ssl else "", host, port + ), data={'_http_id': http_id, 'exec': 'devlist'}, auth=requests.auth.HTTPBasicAuth(username, password)).prepare() self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") - self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato")) self.last_results = {"wldev": [], "dhcpd_lease": []} self.success_init = self._update_tomato_info() @@ -74,10 +87,16 @@ class TomatoDeviceScanner(DeviceScanner): Return boolean if scanning successful. """ - self.logger.info("Scanning") + _LOGGER.info("Scanning") try: - response = requests.Session().send(self.req, timeout=3) + if self.ssl: + response = requests.Session().send(self.req, + timeout=3, + verify=self.verify_ssl) + else: + response = requests.Session().send(self.req, timeout=3) + # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. if response.status_code == 200: @@ -92,7 +111,7 @@ class TomatoDeviceScanner(DeviceScanner): elif response.status_code == 401: # Authentication error - self.logger.exception(( + _LOGGER.exception(( "Failed to authenticate, " "please check your username and password")) return False @@ -100,17 +119,17 @@ class TomatoDeviceScanner(DeviceScanner): except requests.exceptions.ConnectionError: # We get this if we could not connect to the router or # an invalid http_id was supplied. - self.logger.exception("Failed to connect to the router or " - "invalid http_id supplied") + _LOGGER.exception("Failed to connect to the router or " + "invalid http_id supplied") return False except requests.exceptions.Timeout: # We get this if we could not connect to the router or # an invalid http_id was supplied. - self.logger.exception("Connection to the router timed out") + _LOGGER.exception("Connection to the router timed out") return False except ValueError: # If JSON decoder could not parse the response. - self.logger.exception("Failed to parse response from router") + _LOGGER.exception("Failed to parse response from router") return False diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 99f20d4385e..2306a66070b 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -11,11 +11,11 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -30,8 +30,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_DHCP_SOFTWARE, - default=DEFAULT_DHCP_SOFTWARE): vol.In(DHCP_SOFTWARES) + vol.Optional(CONF_DHCP_SOFTWARE, default=DEFAULT_DHCP_SOFTWARE): + vol.In(DHCP_SOFTWARES), }) @@ -49,14 +49,14 @@ def get_scanner(hass, config): def _refresh_on_acccess_denied(func): """If remove rebooted, it lost our session so rebuld one and try again.""" def decorator(self, *args, **kwargs): - """Wrapper function to refresh session_id on PermissionError.""" + """Wrap the function to refresh session_id on PermissionError.""" try: return func(self, *args, **kwargs) except PermissionError: _LOGGER.warning("Invalid session detected." + - " Tryign to refresh session_id and re-run the rpc") - self.session_id = _get_session_id(self.url, self.username, - self.password) + " Trying to refresh session_id and re-run RPC") + self.session_id = _get_session_id( + self.url, self.username, self.password) return func(self, *args, **kwargs) @@ -80,8 +80,8 @@ class UbusDeviceScanner(DeviceScanner): self.last_results = {} self.url = 'http://{}/ubus'.format(host) - self.session_id = _get_session_id(self.url, self.username, - self.password) + self.session_id = _get_session_id( + self.url, self.username, self.password) self.hostapd = [] self.mac2name = None self.success_init = self.session_id is not None diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py index 57a0186a2e2..168ab04ec6f 100644 --- a/homeassistant/components/device_tracker/unifi_direct.py +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -98,11 +98,15 @@ class UnifiDeviceScanner(DeviceScanner): self.connected = False def _get_update(self): - from pexpect import pxssh + from pexpect import pxssh, exceptions try: if not self.connected: self._connect() + # If we still aren't connected at this point + # don't try to send anything to the AP. + if not self.connected: + return None self.ssh.sendline(UNIFI_COMMAND) self.ssh.prompt() return self.ssh.before @@ -110,7 +114,7 @@ class UnifiDeviceScanner(DeviceScanner): _LOGGER.error("Unexpected SSH error: %s", str(err)) self._disconnect() return None - except AssertionError as err: + except (AssertionError, exceptions.EOF) as err: _LOGGER.error("Connection to AP unavailable: %s", str(err)) self._disconnect() return None diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index fbcd753713c..ea0645e012f 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -84,7 +84,7 @@ class UPCDeviceScanner(DeviceScanner): @asyncio.coroutine def async_get_device_name(self, device): - """The firmware doesn't save the name of the wireless device.""" + """Get the device name (the name of the wireless device not used).""" return None @asyncio.coroutine diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 0c3152db3d6..980ac7d661c 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -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.2.3'] +REQUIREMENTS = ['netdisco==1.2.4'] DOMAIN = 'discovery' @@ -53,6 +53,7 @@ SERVICE_HANDLERS = { SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), + SERVICE_DAIKIN: ('daikin', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index b4bb977ee70..132e230c137 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -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.14'] +REQUIREMENTS = ['python-ecobee-api==0.0.15'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 1a3b6413d2c..b2206f80766 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -130,8 +130,9 @@ class Config(object): self.cached_states = {} if self.type == TYPE_ALEXA: - _LOGGER.warning("Alexa type is deprecated and will be removed in a" - " future version") + _LOGGER.warning( + 'Emulated Hue running in legacy mode because type has been ' + 'specified. More info at https://goo.gl/M6tgz8') # Get the IP address that will be passed to the Echo during discovery self.host_ip_addr = conf.get(CONF_HOST_IP) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index f2630aa98d2..c5e5b8736ae 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -3,38 +3,42 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.dyson/ """ -import logging import asyncio -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, - DOMAIN) -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.dyson import DYSON_DEVICES +import logging -DEPENDENCIES = ['dyson'] +import voluptuous as vol + +from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.components.fan import ( + DOMAIN, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) +from homeassistant.const import CONF_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) +CONF_NIGHT_MODE = 'night_mode' + +DEPENDENCIES = ['dyson'] +DYSON_FAN_DEVICES = 'dyson_fan_devices' -DYSON_FAN_DEVICES = "dyson_fan_devices" SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode' DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({ - vol.Required('entity_id'): cv.entity_id, - vol.Required('night_mode'): cv.boolean + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_NIGHT_MODE): cv.boolean, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Dyson fan components.""" - _LOGGER.info("Creating new Dyson fans") + """Set up the Dyson fan components.""" + from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink + + _LOGGER.debug("Creating new Dyson fans") if DYSON_FAN_DEVICES not in hass.data: hass.data[DYSON_FAN_DEVICES] = [] # Get Dyson Devices from parent component - from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink for device in [d for d in hass.data[DYSON_DEVICES] if isinstance(d, DysonPureCoolLink)]: dyson_entity = DysonPureCoolLinkDevice(hass, device) @@ -43,9 +47,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(hass.data[DYSON_FAN_DEVICES]) def service_handle(service): - """Handle dyson services.""" - entity_id = service.data.get('entity_id') - night_mode = service.data.get('night_mode') + """Handle the Dyson services.""" + entity_id = service.data.get(CONF_ENTITY_ID) + night_mode = service.data.get(CONF_NIGHT_MODE) fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if fan.entity_id == entity_id].__iter__(), None) if fan_device is None: @@ -57,9 +61,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fan_device.night_mode(night_mode) # Register dyson service(s) - hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE, - service_handle, - schema=DYSON_SET_NIGHT_MODE_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_SET_NIGHT_MODE, service_handle, + schema=DYSON_SET_NIGHT_MODE_SCHEMA) class DysonPureCoolLinkDevice(FanEntity): @@ -67,21 +71,22 @@ class DysonPureCoolLinkDevice(FanEntity): def __init__(self, hass, device): """Initialize the fan.""" - _LOGGER.info("Creating device %s", device.name) + _LOGGER.debug("Creating device %s", device.name) self.hass = hass self._device = device @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) def on_message(self, message): - """Called when new messages received from the fan.""" + """Call when new messages received from the fan.""" from libpurecoollink.dyson_pure_state import DysonPureCoolState + if isinstance(message, DysonPureCoolState): - _LOGGER.debug("Message received for fan device %s : %s", self.name, + _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @@ -97,41 +102,46 @@ class DysonPureCoolLinkDevice(FanEntity): def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan. Never called ??.""" - _LOGGER.debug("Set fan speed to: " + speed) from libpurecoollink.const import FanSpeed, FanMode + + _LOGGER.debug("Set fan speed to: %s", speed) + if speed == FanSpeed.FAN_SPEED_AUTO.value: self._device.set_configuration(fan_mode=FanMode.AUTO) else: fan_speed = FanSpeed('{0:04d}'.format(int(speed))) - self._device.set_configuration(fan_mode=FanMode.FAN, - fan_speed=fan_speed) + self._device.set_configuration( + fan_mode=FanMode.FAN, fan_speed=fan_speed) def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: """Turn on the fan.""" - _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) from libpurecoollink.const import FanSpeed, FanMode + + _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed: if speed == FanSpeed.FAN_SPEED_AUTO.value: self._device.set_configuration(fan_mode=FanMode.AUTO) else: fan_speed = FanSpeed('{0:04d}'.format(int(speed))) - self._device.set_configuration(fan_mode=FanMode.FAN, - fan_speed=fan_speed) + self._device.set_configuration( + fan_mode=FanMode.FAN, fan_speed=fan_speed) else: # Speed not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) def turn_off(self: ToggleEntity, **kwargs) -> None: """Turn off the fan.""" - _LOGGER.debug("Turn off fan %s", self.name) from libpurecoollink.const import FanMode + + _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) def oscillate(self: ToggleEntity, oscillating: bool) -> None: """Turn on/off oscillating.""" + from libpurecoollink.const import Oscillation + _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) - from libpurecoollink.const import Oscillation if oscillating: self._device.set_configuration( @@ -155,8 +165,9 @@ class DysonPureCoolLinkDevice(FanEntity): @property def speed(self) -> str: """Return the current speed.""" + from libpurecoollink.const import FanSpeed + if self._device.state: - from libpurecoollink.const import FanSpeed if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed return int(self._device.state.speed) @@ -174,8 +185,9 @@ class DysonPureCoolLinkDevice(FanEntity): def night_mode(self: ToggleEntity, night_mode: bool) -> None: """Turn fan in night mode.""" - _LOGGER.debug("Set %s night mode %s", self.name, night_mode) from libpurecoollink.const import NightMode + + _LOGGER.debug("Set %s night mode %s", self.name, night_mode) if night_mode: self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) else: @@ -188,8 +200,9 @@ class DysonPureCoolLinkDevice(FanEntity): def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: """Turn fan in auto mode.""" - _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) from libpurecoollink.const import FanMode + + _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) if auto_mode: self._device.set_configuration(fan_mode=FanMode.AUTO) else: @@ -199,17 +212,20 @@ class DysonPureCoolLinkDevice(FanEntity): def speed_list(self: ToggleEntity) -> list: """Get the list of available speeds.""" from libpurecoollink.const import FanSpeed - supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value, - int(FanSpeed.FAN_SPEED_1.value), - int(FanSpeed.FAN_SPEED_2.value), - int(FanSpeed.FAN_SPEED_3.value), - int(FanSpeed.FAN_SPEED_4.value), - int(FanSpeed.FAN_SPEED_5.value), - int(FanSpeed.FAN_SPEED_6.value), - int(FanSpeed.FAN_SPEED_7.value), - int(FanSpeed.FAN_SPEED_8.value), - int(FanSpeed.FAN_SPEED_9.value), - int(FanSpeed.FAN_SPEED_10.value)] + + supported_speeds = [ + FanSpeed.FAN_SPEED_AUTO.value, + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value), + ] return supported_speeds diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 2a8ad453ec8..a306cf7767c 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -63,3 +63,65 @@ dyson_set_night_mode: night_mode: description: Night mode status example: true + +xiaomi_miio_set_buzzer_on: + description: Turn the buzzer on. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_buzzer_off: + description: Turn the buzzer off. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_led_on: + description: Turn the led on. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_led_off: + description: Turn the led off. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_child_lock_on: + description: Turn the child lock on. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_child_lock_off: + description: Turn the child lock off. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + +xiaomi_miio_set_favorite_level: + description: Set the favorite level. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + level: + description: Level, between 0 and 16. + example: 1 + +xiaomi_miio_set_led_brightness: + description: Set the led brightness. + fields: + entity_id: + description: Name of the air purifier entity. + example: 'fan.xiaomi_air_purifier' + brightness: + description: Brightness (0 = Bright, 1 = Dim, 2 = Off) + example: 1 diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 3920e606d90..827f134cc08 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -7,20 +7,18 @@ https://home-assistant.io/components/fan.wink/ import asyncio import logging -from homeassistant.components.fan import (FanEntity, SPEED_HIGH, - SPEED_LOW, SPEED_MEDIUM, - STATE_UNKNOWN, SUPPORT_SET_SPEED, - SUPPORT_DIRECTION) +from homeassistant.components.fan import ( + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN, SUPPORT_DIRECTION, + SUPPORT_SET_SPEED, FanEntity) +from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.wink import WinkDevice, DOMAIN - -DEPENDENCIES = ['wink'] _LOGGER = logging.getLogger(__name__) -SPEED_LOWEST = 'lowest' -SPEED_AUTO = 'auto' +DEPENDENCIES = ['wink'] +SPEED_AUTO = 'auto' +SPEED_LOWEST = 'lowest' SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED @@ -38,7 +36,7 @@ class WinkFanDevice(WinkDevice, FanEntity): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['fan'].append(self) def set_direction(self: ToggleEntity, direction: str) -> None: diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 9f21fda408d..910e33627a6 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.3'] +REQUIREMENTS = ['python-miio==0.3.4'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' @@ -43,6 +43,8 @@ ATTR_CHILD_LOCK = 'child_lock' ATTR_LED = 'led' ATTR_LED_BRIGHTNESS = 'led_brightness' ATTR_MOTOR_SPEED = 'motor_speed' +ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' +ATTR_PURIFY_VOLUME = 'purify_volume' ATTR_BRIGHTNESS = 'brightness' ATTR_LEVEL = 'level' @@ -53,6 +55,8 @@ SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' +SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' +SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness' @@ -75,6 +79,8 @@ SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, SERVICE_SET_LED_ON: {'method': 'async_set_led_on'}, SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, + SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, + SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, SERVICE_SET_FAVORITE_LEVEL: { 'method': 'async_set_favorite_level', 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, @@ -116,15 +122,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - target_air_purifiers = [air for air in hass.data[PLATFORM].values() - if air.entity_id in entity_ids] + devices = [device for device in hass.data[PLATFORM].values() if + device.entity_id in entity_ids] else: - target_air_purifiers = hass.data[PLATFORM].values() + devices = hass.data[PLATFORM].values() update_tasks = [] - for air_purifier in target_air_purifiers: - yield from getattr(air_purifier, method['method'])(**params) - update_tasks.append(air_purifier.async_update_ha_state(True)) + for device in devices: + yield from getattr(device, method['method'])(**params) + update_tasks.append(device.async_update_ha_state(True)) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) @@ -157,7 +163,9 @@ class XiaomiAirPurifier(FanEntity): ATTR_CHILD_LOCK: None, ATTR_LED: None, ATTR_LED_BRIGHTNESS: None, - ATTR_MOTOR_SPEED: None + ATTR_MOTOR_SPEED: None, + ATTR_AVERAGE_AIR_QUALITY_INDEX: None, + ATTR_PURIFY_VOLUME: None, } @property @@ -244,7 +252,9 @@ class XiaomiAirPurifier(FanEntity): ATTR_BUZZER: state.buzzer, ATTR_CHILD_LOCK: state.child_lock, ATTR_LED: state.led, - ATTR_MOTOR_SPEED: state.motor_speed + ATTR_MOTOR_SPEED: state.motor_speed, + ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi, + ATTR_PURIFY_VOLUME: state.purify_volume, } if state.led_brightness: @@ -284,30 +294,44 @@ class XiaomiAirPurifier(FanEntity): def async_set_buzzer_on(self): """Turn the buzzer on.""" yield from self._try_command( - "Turning the buzzer of air purifier on failed.", + "Turning the buzzer of the air purifier on failed.", self._air_purifier.set_buzzer, True) @asyncio.coroutine def async_set_buzzer_off(self): - """Turn the buzzer on.""" + """Turn the buzzer off.""" yield from self._try_command( - "Turning the buzzer of air purifier off failed.", + "Turning the buzzer of the air purifier off failed.", self._air_purifier.set_buzzer, False) @asyncio.coroutine def async_set_led_on(self): """Turn the led on.""" yield from self._try_command( - "Turning the led of air purifier off failed.", + "Turning the led of the air purifier off failed.", self._air_purifier.set_led, True) @asyncio.coroutine def async_set_led_off(self): """Turn the led off.""" yield from self._try_command( - "Turning the led of air purifier off failed.", + "Turning the led of the air purifier off failed.", self._air_purifier.set_led, False) + @asyncio.coroutine + def async_set_child_lock_on(self): + """Turn the child lock on.""" + yield from self._try_command( + "Turning the child lock of the air purifier on failed.", + self._air_purifier.set_child_lock, True) + + @asyncio.coroutine + def async_set_child_lock_off(self): + """Turn the child lock off.""" + yield from self._try_command( + "Turning the child lock of the air purifier off failed.", + self._air_purifier.set_child_lock, False) + @asyncio.coroutine def async_set_led_brightness(self, brightness: int=2): """Set the led brightness.""" diff --git a/homeassistant/components/fan/xiaomi_miio_services.yaml b/homeassistant/components/fan/xiaomi_miio_services.yaml deleted file mode 100644 index 93f6318e60b..00000000000 --- a/homeassistant/components/fan/xiaomi_miio_services.yaml +++ /dev/null @@ -1,56 +0,0 @@ - -xiaomi_miio_set_buzzer_on: - description: Turn the buzzer on. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - -xiaomi_miio_set_buzzer_off: - description: Turn the buzzer off. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - -xiaomi_miio_set_led_on: - description: Turn the led on. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - -xiaomi_miio_set_led_off: - description: Turn the led off. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - -xiaomi_miio_set_favorite_level: - description: Set the favorite level. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - - level: - description: Level, between 0 and 16. - example: '1' - -xiaomi_miio_set_led_brightness: - description: Set the led brightness. - - fields: - entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' - - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: '1' diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7d19ed46cd9..8f5a18ff843 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180112.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180126.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -260,10 +260,10 @@ def async_register_panel(hass, component_name, path, md5=None, component_name: name of the web component path: path to the HTML of the web component (required unless url is provided) - md5: the md5 hash of the web component (for versioning in url, optional) + md5: the md5 hash of the web component (for versioning in URL, optional) sidebar_title: title to show in the sidebar (optional) sidebar_icon: icon to show next to title in sidebar (optional) - url_path: name to use in the url (defaults to component_name) + url_path: name to use in the URL (defaults to component_name) config: config to be passed into the web component """ panel = ExternalPanel(component_name, path, md5, sidebar_title, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index fc250c4b655..0483f424ca3 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -18,7 +18,8 @@ DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ 'switch', 'light', 'group', 'media_player', 'fan', 'cover', 'climate' ] -CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', 'heatcool'} +CLIMATE_MODE_HEATCOOL = 'heatcool' +CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} PREFIX_TRAITS = 'action.devices.traits.' TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0faa9bdc484..d8e9f668c8e 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -14,13 +14,14 @@ from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.util.decorator import Registry from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_NAME, CONF_TYPE ) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate + switch, light, cover, media_player, group, fan, scene, script, climate, + sensor ) from homeassistant.util.unit_system import METRIC_SYSTEM @@ -32,7 +33,7 @@ from .const import ( TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP, TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING, TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT, - CONF_ALIASES, CLIMATE_SUPPORTED_MODES + CONF_ALIASES, CLIMATE_SUPPORTED_MODES, CLIMATE_MODE_HEATCOOL ) HANDLERS = Registry() @@ -67,6 +68,23 @@ MAPPING_COMPONENT = { } # type: Dict[str, list] +"""Error code used for SmartHomeError class.""" +ERROR_NOT_SUPPORTED = "notSupported" + + +class SmartHomeError(Exception): + """Google Assistant Smart Home errors.""" + + def __init__(self, code, msg): + """Log error code.""" + super(SmartHomeError, self).__init__(msg) + _LOGGER.error( + "An error has ocurred in Google SmartHome: %s." + "Error code: %s", msg, code + ) + self.code = code + + class Config: """Hold the configuration for Google Assistant.""" @@ -80,8 +98,9 @@ class Config: def entity_to_device(entity: Entity, config: Config, units: UnitSystem): """Convert a hass entity into an google actions device.""" entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) class_data = MAPPING_COMPONENT.get( - entity_config.get(CONF_TYPE) or entity.domain) + google_domain or entity.domain) if class_data is None: return None @@ -128,30 +147,92 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): entity.attributes.get(light.ATTR_MIN_MIREDS)))) if entity.domain == climate.DOMAIN: - modes = ','.join( - m.lower() for m in entity.attributes.get( - climate.ATTR_OPERATION_LIST, []) - if m.lower() in CLIMATE_SUPPORTED_MODES) + modes = [] + for mode in entity.attributes.get(climate.ATTR_OPERATION_LIST, []): + if mode in CLIMATE_SUPPORTED_MODES: + modes.append(mode) + elif mode == climate.STATE_AUTO: + modes.append(CLIMATE_MODE_HEATCOOL) + device['attributes'] = { - 'availableThermostatModes': modes, + 'availableThermostatModes': ','.join(modes), 'thermostatTemperatureUnit': 'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C', } _LOGGER.debug('Thermostat attributes %s', device['attributes']) + + if entity.domain == sensor.DOMAIN: + if google_domain == climate.DOMAIN: + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + + device['attributes'] = { + 'thermostatTemperatureUnit': + 'F' if unit_of_measurement == TEMP_FAHRENHEIT else 'C', + } + _LOGGER.debug('Sensor attributes %s', device['attributes']) + return device -def query_device(entity: Entity, units: UnitSystem) -> dict: +def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: """Take an entity and return a properly formatted device object.""" def celsius(deg: Optional[float]) -> Optional[float]: """Convert a float to Celsius and rounds to one decimal place.""" if deg is None: return None return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) + + if entity.domain == sensor.DOMAIN: + entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) + + if google_domain == climate.DOMAIN: + # check if we have a string value to convert it to number + value = entity.state + if isinstance(entity.state, str): + try: + value = float(value) + except ValueError: + value = None + + if value is None: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Invalid value {} for the climate sensor" + .format(entity.state) + ) + + # detect if we report temperature or humidity + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: + value = celsius(value) + attr = 'thermostatTemperatureAmbient' + elif unit_of_measurement == '%': + attr = 'thermostatHumidityAmbient' + else: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Unit {} is not supported by the climate sensor" + .format(unit_of_measurement) + ) + + return {attr: value} + + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Sensor type {} is not supported".format(google_domain) + ) + if entity.domain == climate.DOMAIN: mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() if mode not in CLIMATE_SUPPORTED_MODES: - mode = 'on' + mode = 'heat' response = { 'thermostatMode': mode, 'thermostatTemperatureSetpoint': @@ -245,9 +326,9 @@ def determine_service( # special climate handling if domain == climate.DOMAIN: if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - service_data['temperature'] = units.temperature( - params.get('thermostatTemperatureSetpoint', 25), - TEMP_CELSIUS) + service_data['temperature'] = \ + units.temperature( + params['thermostatTemperatureSetpoint'], TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: service_data['target_temp_high'] = units.temperature( @@ -258,8 +339,12 @@ def determine_service( TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_SET_MODE: - service_data['operation_mode'] = params.get( - 'thermostatMode', 'off') + mode = params['thermostatMode'] + + if mode == CLIMATE_MODE_HEATCOOL: + mode = climate.STATE_AUTO + + service_data['operation_mode'] = mode return (climate.SERVICE_SET_OPERATION_MODE, service_data) if command == COMMAND_BRIGHTNESS: @@ -317,7 +402,7 @@ def async_handle_message(hass, config, message): @HANDLERS.register('action.devices.SYNC') @asyncio.coroutine -def async_devices_sync(hass, config, payload): +def async_devices_sync(hass, config: Config, payload): """Handle action.devices.SYNC request.""" devices = [] for entity in hass.states.async_all(): @@ -354,7 +439,10 @@ def async_devices_query(hass, config, payload): # If we can't find a state, the device is offline devices[devid] = {'online': False} - devices[devid] = query_device(state, hass.config.units) + try: + devices[devid] = query_device(state, config, hass.config.units) + except SmartHomeError as error: + devices[devid] = {'errorCode': error.code} return {'devices': devices} diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 8b1e05e3122..a8529f18b69 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -42,8 +42,6 @@ ATTR_ORDER = 'order' ATTR_VIEW = 'view' ATTR_VISIBLE = 'visible' -DATA_ALL_GROUPS = 'data_all_groups' - SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET = 'set' SERVICE_REMOVE = 'remove' @@ -250,8 +248,10 @@ def get_entity_ids(hass, entity_id, domain_filter=None): @asyncio.coroutine def async_setup(hass, config): """Set up all groups found definded in the configuration.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - hass.data[DATA_ALL_GROUPS] = {} + component = hass.data.get(DOMAIN) + + if component is None: + component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) yield from _async_process_config(hass, config, component) @@ -271,10 +271,11 @@ def async_setup(hass, config): def groups_service_handler(service): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] - service_groups = hass.data[DATA_ALL_GROUPS] + entity_id = ENTITY_ID_FORMAT.format(object_id) + group = component.get_entity(entity_id) # new group - if service.service == SERVICE_SET and object_id not in service_groups: + if service.service == SERVICE_SET and group is None: entity_ids = service.data.get(ATTR_ENTITIES) or \ service.data.get(ATTR_ADD_ENTITIES) or None @@ -289,12 +290,15 @@ def async_setup(hass, config): user_defined=False, **extra_arg ) + return + if group is None: + _LOGGER.warning("%s:Group '%s' doesn't exist!", + service.service, object_id) return # update group if service.service == SERVICE_SET: - group = service_groups[object_id] need_update = False if ATTR_ADD_ENTITIES in service.data: @@ -333,12 +337,7 @@ def async_setup(hass, config): # remove group if service.service == SERVICE_REMOVE: - if object_id not in service_groups: - _LOGGER.warning("Group '%s' doesn't exist!", object_id) - return - - del_group = service_groups.pop(object_id) - yield from del_group.async_stop() + yield from component.async_remove_entity(entity_id) hass.services.async_register( DOMAIN, SERVICE_SET, groups_service_handler, @@ -395,7 +394,7 @@ class Group(Entity): """Track a group of entity ids.""" def __init__(self, hass, name, order=None, visible=True, icon=None, - view=False, control=None, user_defined=True): + view=False, control=None, user_defined=True, entity_ids=None): """Initialize a group. This Object has factory function for creation. @@ -405,7 +404,10 @@ class Group(Entity): self._state = STATE_UNKNOWN self._icon = icon self.view = view - self.tracking = [] + if entity_ids: + self.tracking = tuple(ent_id.lower() for ent_id in entity_ids) + else: + self.tracking = tuple() self.group_on = None self.group_off = None self.visible = visible @@ -439,23 +441,21 @@ class Group(Entity): hass, name, order=len(hass.states.async_entity_ids(DOMAIN)), visible=visible, icon=icon, view=view, control=control, - user_defined=user_defined + user_defined=user_defined, entity_ids=entity_ids ) group.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id or name, hass=hass) - # run other async stuff - if entity_ids is not None: - yield from group.async_update_tracked_entity_ids(entity_ids) - else: - yield from group.async_update_ha_state(True) - # If called before the platform async_setup is called (test cases) - if DATA_ALL_GROUPS not in hass.data: - hass.data[DATA_ALL_GROUPS] = {} + component = hass.data.get(DOMAIN) + + if component is None: + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([group], True) - hass.data[DATA_ALL_GROUPS][object_id] = group return group @property @@ -534,10 +534,6 @@ class Group(Entity): yield from self.async_update_ha_state(True) self.async_start() - def start(self): - """Start tracking members.""" - self.hass.add_job(self.async_start) - @callback def async_start(self): """Start tracking members. @@ -549,17 +545,15 @@ class Group(Entity): self.hass, self.tracking, self._async_state_changed_listener ) - def stop(self): - """Unregister the group from Home Assistant.""" - run_coroutine_threadsafe(self.async_stop(), self.hass.loop).result() - @asyncio.coroutine def async_stop(self): """Unregister the group from Home Assistant. This method must be run in the event loop. """ - yield from self.async_remove() + if self._async_unsub_state_changed: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None @asyncio.coroutine def async_update(self): @@ -567,17 +561,19 @@ class Group(Entity): self._state = STATE_UNKNOWN self._async_update_group_state() - def async_remove(self): - """Remove group from HASS. + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when added to HASS.""" + if self.tracking: + self.async_start() - This method must be run in the event loop and returns a coroutine. - """ + @asyncio.coroutine + def async_will_remove_from_hass(self): + """Callback when removed from HASS.""" if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None - return super().async_remove() - @asyncio.coroutine def _async_state_changed_listener(self, entity_id, old_state, new_state): """Respond to a member state changing. diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index cc6db5fbab3..510b08e766f 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -12,21 +12,22 @@ import re import aiohttp from aiohttp import web -from aiohttp.web_exceptions import HTTPBadGateway from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.web_exceptions import HTTPBadGateway import async_timeout import voluptuous as vol -from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.const import ( - CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE, - SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.components.http import ( - HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT, - CONF_SERVER_HOST, CONF_SSL_CERTIFICATE) -from homeassistant.loader import bind_hass + CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT, + CONF_SSL_CERTIFICATE, KEY_AUTHENTICATED, HomeAssistantView) +from homeassistant.const import ( + CONF_TIME_ZONE, CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, + SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) +from homeassistant.core import DOMAIN as HASS_DOMAIN +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,8 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^panel_(es5|latest)$'), re.compile(r'^addons/[^/]*/logo$') + re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^addons/[^/]*/logo$') } SCHEMA_NO_DATA = vol.Schema({}) @@ -126,7 +128,7 @@ MAP_SERVICE_API = { @callback @bind_hass def get_homeassistant_version(hass): - """Return latest available HomeAssistant version. + """Return latest available Home Assistant version. Async friendly. """ @@ -136,7 +138,7 @@ def get_homeassistant_version(hass): @callback @bind_hass def is_hassio(hass): - """Return True if hass.io is loaded. + """Return true if hass.io is loaded. Async friendly. """ @@ -146,7 +148,7 @@ def is_hassio(hass): @bind_hass @asyncio.coroutine def async_check_config(hass): - """Check config over Hass.io API.""" + """Check configuration over Hass.io API.""" result = yield from hass.data[DOMAIN].send_command( '/homeassistant/check', timeout=300) @@ -159,18 +161,18 @@ def async_check_config(hass): @asyncio.coroutine def async_setup(hass, config): - """Set up the HASSio component.""" + """Set up the Hass.io component.""" try: host = os.environ['HASSIO'] except KeyError: - _LOGGER.error("No HassIO supervisor detect!") + _LOGGER.error("No Hass.io supervisor detect") return False websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) if not (yield from hassio.is_connected()): - _LOGGER.error("Not connected with HassIO!") + _LOGGER.error("Not connected with Hass.io") return False hass.http.register_view(HassIOView(hassio)) @@ -187,7 +189,7 @@ def async_setup(hass, config): @asyncio.coroutine def async_service_handler(service): - """Handle service calls for HassIO.""" + """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] data = service.data.copy() addon = data.pop(ATTR_ADDON, None) @@ -215,7 +217,7 @@ def async_setup(hass, config): @asyncio.coroutine def update_homeassistant_version(now): - """Update last available HomeAssistant version.""" + """Update last available Home Assistant version.""" data = yield from hassio.get_homeassistant_info() if data: hass.data[DATA_HOMEASSISTANT_VERSION] = \ @@ -255,10 +257,10 @@ def async_setup(hass, config): def _api_bool(funct): - """API wrapper to return Boolean.""" + """Return a boolean.""" @asyncio.coroutine def _wrapper(*argv, **kwargs): - """Wrapper function.""" + """Wrap function.""" data = yield from funct(*argv, **kwargs) return data and data['result'] == "ok" @@ -266,24 +268,24 @@ def _api_bool(funct): class HassIO(object): - """Small API wrapper for HassIO.""" + """Small API wrapper for Hass.io.""" def __init__(self, loop, websession, ip): - """Initialze HassIO api.""" + """Initialize Hass.io API.""" self.loop = loop self.websession = websession self._ip = ip @_api_bool def is_connected(self): - """Return True if it connected to HassIO supervisor. + """Return true if it connected to Hass.io supervisor. This method return a coroutine. """ return self.send_command("/supervisor/ping", method="get") def get_homeassistant_info(self): - """Return data for HomeAssistant. + """Return data for Home Assistant. This method return a coroutine. """ @@ -291,7 +293,7 @@ class HassIO(object): @_api_bool def update_hass_api(self, http_config): - """Update Home-Assistant API data on HassIO. + """Update Home Assistant API data on Hass.io. This method return a coroutine. """ @@ -305,13 +307,13 @@ class HassIO(object): if CONF_SERVER_HOST in http_config: options['watchdog'] = False - _LOGGER.warning("Don't use 'server_host' options with Hass.io!") + _LOGGER.warning("Don't use 'server_host' options with Hass.io") return self.send_command("/homeassistant/options", payload=options) @_api_bool def update_hass_timezone(self, core_config): - """Update Home-Assistant timezone data on HassIO. + """Update Home-Assistant timezone data on Hass.io. This method return a coroutine. """ @@ -321,7 +323,7 @@ class HassIO(object): @asyncio.coroutine def send_command(self, command, method="post", payload=None, timeout=10): - """Send API command to HassIO. + """Send API command to Hass.io. This method is a coroutine. """ @@ -330,7 +332,7 @@ class HassIO(object): request = yield from self.websession.request( method, "http://{}{}".format(self._ip, command), json=payload, headers={ - X_HASSIO: os.environ.get('HASSIO_TOKEN') + X_HASSIO: os.environ.get('HASSIO_TOKEN', "") }) if request.status not in (200, 400): @@ -351,7 +353,7 @@ class HassIO(object): @asyncio.coroutine def command_proxy(self, path, request): - """Return a client request with proxy origin for HassIO supervisor. + """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ @@ -359,7 +361,7 @@ class HassIO(object): try: data = None - headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN')} + headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")} with async_timeout.timeout(10, loop=self.loop): data = yield from request.read() if data: @@ -376,28 +378,28 @@ class HassIO(object): return client except aiohttp.ClientError as err: - _LOGGER.error("Client error on api %s request %s.", path, err) + _LOGGER.error("Client error on api %s request %s", path, err) except asyncio.TimeoutError: - _LOGGER.error("Client timeout error on api request %s.", path) + _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway() class HassIOView(HomeAssistantView): - """HassIO view to handle base part.""" + """Hass.io view to handle base part.""" name = "api:hassio" url = "/api/hassio/{path:.+}" requires_auth = False def __init__(self, hassio): - """Initialize a hassio base view.""" + """Initialize a Hass.io base view.""" self.hassio = hassio @asyncio.coroutine def _handle(self, request, path): - """Route data to hassio.""" + """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: return web.Response(status=401) @@ -434,7 +436,7 @@ def _create_response_log(client, data): def _get_timeout(path): - """Return timeout for a url path.""" + """Return timeout for a URL path.""" for re_path in NO_TIMEOUT: if re_path.match(path): return 0 @@ -442,7 +444,7 @@ def _get_timeout(path): def _need_auth(path): - """Return if a path need a auth.""" + """Return if a path need authentication.""" for re_path in NO_AUTH: if re_path.match(path): return False diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py index bf5196d6582..abe52ebe98a 100644 --- a/homeassistant/components/hive.py +++ b/homeassistant/components/hive.py @@ -1,80 +1,80 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hive/ -""" -import logging -import voluptuous as vol - -from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, - CONF_USERNAME) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform - -REQUIREMENTS = ['pyhiveapi==0.2.10'] - -_LOGGER = logging.getLogger(__name__) -DOMAIN = 'hive' -DATA_HIVE = 'data_hive' -DEVICETYPES = { - 'binary_sensor': 'device_list_binary_sensor', - 'climate': 'device_list_climate', - 'light': 'device_list_light', - 'switch': 'device_list_plug', - 'sensor': 'device_list_sensor', - } - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) - - -class HiveSession: - """Initiate Hive Session Class.""" - - entities = [] - core = None - heating = None - hotwater = None - light = None - sensor = None - switch = None - - -def setup(hass, config): - """Set up the Hive Component.""" - from pyhiveapi import Pyhiveapi - - session = HiveSession() - session.core = Pyhiveapi() - - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - - devicelist = session.core.initialise_api(username, - password, - update_interval) - - if devicelist is None: - _LOGGER.error("Hive API initialization failed") - return False - - session.sensor = Pyhiveapi.Sensor() - session.heating = Pyhiveapi.Heating() - session.hotwater = Pyhiveapi.Hotwater() - session.light = Pyhiveapi.Light() - session.switch = Pyhiveapi.Switch() - hass.data[DATA_HIVE] = session - - for ha_type, hive_type in DEVICETYPES.items(): - for key, devices in devicelist.items(): - if key == hive_type: - for hivedevice in devices: - load_platform(hass, ha_type, DOMAIN, hivedevice, config) - return True +""" +Support for the Hive devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/hive/ +""" +import logging +import voluptuous as vol + +from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['pyhiveapi==0.2.11'] + +_LOGGER = logging.getLogger(__name__) +DOMAIN = 'hive' +DATA_HIVE = 'data_hive' +DEVICETYPES = { + 'binary_sensor': 'device_list_binary_sensor', + 'climate': 'device_list_climate', + 'light': 'device_list_light', + 'switch': 'device_list_plug', + 'sensor': 'device_list_sensor', + } + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, + }) +}, extra=vol.ALLOW_EXTRA) + + +class HiveSession: + """Initiate Hive Session Class.""" + + entities = [] + core = None + heating = None + hotwater = None + light = None + sensor = None + switch = None + + +def setup(hass, config): + """Set up the Hive Component.""" + from pyhiveapi import Pyhiveapi + + session = HiveSession() + session.core = Pyhiveapi() + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + + devicelist = session.core.initialise_api(username, + password, + update_interval) + + if devicelist is None: + _LOGGER.error("Hive API initialization failed") + return False + + session.sensor = Pyhiveapi.Sensor() + session.heating = Pyhiveapi.Heating() + session.hotwater = Pyhiveapi.Hotwater() + session.light = Pyhiveapi.Light() + session.switch = Pyhiveapi.Switch() + hass.data[DATA_HIVE] = session + + for ha_type, hive_type in DEVICETYPES.items(): + for key, devices in devicelist.items(): + if key == hive_type: + for hivedevice in devices: + load_platform(hass, ha_type, DOMAIN, hivedevice, config) + return True diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b2f6384d467..db2a43d8728 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -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.37'] +REQUIREMENTS = ['pyhomematic==0.1.38'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index a83b55e84e5..36d5a1a56a0 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -9,6 +9,7 @@ import logging import os import socket +import requests import voluptuous as vol from homeassistant.components.discovery import SERVICE_HUE @@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "hue" SERVICE_HUE_SCENE = "hue_activate_scene" +API_NUPNP = 'https://www.meethue.com/api/nupnp' CONF_BRIDGES = "bridges" @@ -49,7 +51,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema([{ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_BRIDGES, default=[]): BRIDGE_CONFIG_SCHEMA, + vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) @@ -69,9 +71,9 @@ Press the button on the bridge to register Philips Hue with Home Assistant. def setup(hass, config): """Set up the Hue platform.""" - config = config.get(DOMAIN) - if config is None: - config = {} + conf = config.get(DOMAIN) + if conf is None: + conf = {} if DOMAIN not in hass.data: hass.data[DOMAIN] = {} @@ -82,7 +84,21 @@ def setup(hass, config): lambda service, discovery_info: bridge_discovered(hass, service, discovery_info)) - bridges = config.get(CONF_BRIDGES, []) + # User has configured bridges + if CONF_BRIDGES in conf: + bridges = conf[CONF_BRIDGES] + # Component is part of config but no bridges specified, discover. + elif DOMAIN in config: + # discover from nupnp + hosts = requests.get(API_NUPNP).json() + bridges = [{ + CONF_HOST: entry['internalipaddress'], + CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), + } for entry in hosts] + else: + # Component not specified in config, we're loaded via discovery + bridges = [] + for bridge in bridges: filename = bridge.get(CONF_FILENAME) allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py new file mode 100644 index 00000000000..f3cd9d79046 --- /dev/null +++ b/homeassistant/components/ihc/__init__.py @@ -0,0 +1,213 @@ +"""IHC component. + +For more details about this component, please refer to the documentation at +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) +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) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +REQUIREMENTS = ['ihcsdk==2.1.1'] + +DOMAIN = 'ihc' +IHC_DATA = 'ihc' +IHC_CONTROLLER = 'controller' +IHC_INFO = 'info' +AUTO_SETUP_YAML = 'ihc_auto_setup.yaml' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_URL): cv.string, + 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 + }), +}, extra=vol.ALLOW_EXTRA) + +AUTO_SETUP_SCHEMA = vol.Schema({ + vol.Optional(CONF_BINARY_SENSOR, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_XPATH): cv.string, + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_TYPE, default=None): cv.string, + vol.Optional(CONF_INVERTING, default=False): cv.boolean, + }) + ]), + vol.Optional(CONF_LIGHT, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_XPATH): cv.string, + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_DIMMABLE, default=False): cv.boolean, + }) + ]), + vol.Optional(CONF_SENSOR, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_XPATH): cv.string, + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, + default=TEMP_CELSIUS): cv.string, + }) + ]), + vol.Optional(CONF_SWITCH, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_XPATH): cv.string, + vol.Required(CONF_NODE): cv.string, + }) + ]), +}) + +SET_RUNTIME_VALUE_BOOL_SCHEMA = vol.Schema({ + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Required(ATTR_VALUE): cv.boolean +}) + +SET_RUNTIME_VALUE_INT_SCHEMA = vol.Schema({ + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Required(ATTR_VALUE): int +}) + +SET_RUNTIME_VALUE_FLOAT_SCHEMA = vol.Schema({ + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Required(ATTR_VALUE): vol.Coerce(float) +}) + +_LOGGER = logging.getLogger(__name__) + +IHC_PLATFORMS = ('binary_sensor', 'light', 'sensor', 'switch') + + +def setup(hass, config): + """Setup the IHC component.""" + from ihcsdk.ihccontroller import IHCController + conf = config[DOMAIN] + url = conf[CONF_URL] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + ihc_controller = IHCController(url, username, password) + + if not ihc_controller.authenticate(): + _LOGGER.error("Unable to authenticate on ihc controller.") + return False + + if (conf[CONF_AUTOSETUP] and + not autosetup_ihc_products(hass, config, ihc_controller)): + return False + + hass.data[IHC_DATA] = { + IHC_CONTROLLER: ihc_controller, + IHC_INFO: conf[CONF_INFO]} + + setup_service_functions(hass, ihc_controller) + return True + + +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.") + return False + project = xml.etree.ElementTree.fromstring(project_xml) + + # if an auto setup file exist in the configuration it will override + yaml_path = hass.config.path(AUTO_SETUP_YAML) + if not os.path.isfile(yaml_path): + yaml_path = os.path.join(os.path.dirname(__file__), AUTO_SETUP_YAML) + yaml = load_yaml_config_file(yaml_path) + try: + auto_setup_conf = AUTO_SETUP_SCHEMA(yaml) + except vol.Invalid as exception: + _LOGGER.error("Invalid IHC auto setup data: %s", exception) + return False + groups = project.findall('.//group') + for component in IHC_PLATFORMS: + component_setup = auto_setup_conf[component] + discovery_info = get_discovery_info(component_setup, groups) + if discovery_info: + discovery.load_platform(hass, component, DOMAIN, discovery_info, + config) + return True + + +def get_discovery_info(component_setup, groups): + """Get discovery info for specified component.""" + discovery_data = {} + for group in groups: + groupname = group.attrib['name'] + for product_cfg in component_setup: + products = group.findall(product_cfg[CONF_XPATH]) + for product in products: + nodes = product.findall(product_cfg[CONF_NODE]) + for node in nodes: + if ('setting' in node.attrib + and node.attrib['setting'] == 'yes'): + continue + ihc_id = int(node.attrib['id'].strip('_'), 0) + name = '{}_{}'.format(groupname, ihc_id) + device = { + 'ihc_id': ihc_id, + 'product': product, + 'product_cfg': product_cfg} + discovery_data[name] = device + return discovery_data + + +def setup_service_functions(hass: HomeAssistantType, ihc_controller): + """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] + value = call.data[ATTR_VALUE] + ihc_controller.set_runtime_value_bool(ihc_id, value) + + def set_runtime_value_int(call): + """Set a IHC runtime integer value service function.""" + ihc_id = call.data[ATTR_IHC_ID] + value = call.data[ATTR_VALUE] + ihc_controller.set_runtime_value_int(ihc_id, value) + + def set_runtime_value_float(call): + """Set a IHC runtime float value service function.""" + ihc_id = call.data[ATTR_IHC_ID] + value = call.data[ATTR_VALUE] + ihc_controller.set_runtime_value_float(ihc_id, value) + + hass.services.register(DOMAIN, SERVICE_SET_RUNTIME_VALUE_BOOL, + set_runtime_value_bool, + schema=SET_RUNTIME_VALUE_BOOL_SCHEMA) + hass.services.register(DOMAIN, SERVICE_SET_RUNTIME_VALUE_INT, + set_runtime_value_int, + schema=SET_RUNTIME_VALUE_INT_SCHEMA) + hass.services.register(DOMAIN, SERVICE_SET_RUNTIME_VALUE_FLOAT, + set_runtime_value_float, + schema=SET_RUNTIME_VALUE_FLOAT_SCHEMA) + + +def validate_name(config): + """Validate device name.""" + if CONF_NAME in config: + return config + ihcid = config[CONF_ID] + name = 'ihc_{}'.format(ihcid) + config[CONF_NAME] = name + return config diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py new file mode 100644 index 00000000000..b06746c8e7a --- /dev/null +++ b/homeassistant/components/ihc/const.py @@ -0,0 +1,19 @@ +"""IHC component constants.""" + +CONF_AUTOSETUP = 'auto_setup' +CONF_INFO = 'info' +CONF_XPATH = 'xpath' +CONF_NODE = 'node' +CONF_INVERTING = 'inverting' +CONF_DIMMABLE = 'dimmable' +CONF_BINARY_SENSOR = 'binary_sensor' +CONF_LIGHT = 'light' +CONF_SENSOR = 'sensor' +CONF_SWITCH = 'switch' + +ATTR_IHC_ID = 'ihc_id' +ATTR_VALUE = 'value' + +SERVICE_SET_RUNTIME_VALUE_BOOL = "set_runtime_value_bool" +SERVICE_SET_RUNTIME_VALUE_INT = "set_runtime_value_int" +SERVICE_SET_RUNTIME_VALUE_FLOAT = "set_runtime_value_float" diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml new file mode 100644 index 00000000000..81d5bf37977 --- /dev/null +++ b/homeassistant/components/ihc/ihc_auto_setup.yaml @@ -0,0 +1,98 @@ +# IHC auto setup configuration. +# To customize this, copy this file to the home assistant configuration +# folder and make your changes. + +binary_sensor: + # Magnet contact + - xpath: './/product_dataline[@product_identifier="_0x2109"]' + node: 'dataline_input' + type: 'opening' + inverting: True + # Pir sensors + - xpath: './/product_dataline[@product_identifier="_0x210e"]' + node: 'dataline_input[1]' + type: 'motion' + # Pir sensors twilight sensor + - xpath: './/product_dataline[@product_identifier="_0x0"]' + node: 'dataline_input[1]' + type: 'motion' + # Pir sensors alarm + - xpath: './/product_dataline[@product_identifier="_0x210f"]' + node: 'dataline_input' + type: 'motion' + # Smoke detector + - xpath: './/product_dataline[@product_identifier="_0x210a"]' + node: 'dataline_input' + type: 'smoke' + # leak detector + - xpath: './/product_dataline[@product_identifier="_0x210c"]' + node: 'dataline_input' + type: 'moisture' + # light detector + - xpath: './/product_dataline[@product_identifier="_0x2110"]' + node: 'dataline_input' + type: 'light' + +light: + # Wireless Combi dimmer 4 buttons + - xpath: './/product_airlink[@product_identifier="_0x4406"]' + node: 'airlink_dimming' + dimmable: True + # Wireless Lamp outlet dimmer + - xpath: './/product_airlink[@product_identifier="_0x4304"]' + node: 'airlink_dimming' + dimmable: True + # Wireless universal dimmer + - xpath: './/product_airlink[@product_identifier="_0x4306"]' + node: 'airlink_dimming' + dimmable: True + # Wireless Lamp outlet relay + - xpath: './/product_airlink[@product_identifier="_0x4202"]' + node: 'airlink_relay' + # Wireless Combi relay 4 buttons + - xpath: './/product_airlink[@product_identifier="_0x4404"]' + node: 'airlink_relay' + # Dataline Lamp outlet + - xpath: './/product_dataline[@product_identifier="_0x2202"]' + node: 'dataline_output' + # Mobile Wireless dimmer + - xpath: './/product_airlink[@product_identifier="_0x4303"]' + node: 'airlink_dimming' + dimmable: True + +sensor: + # Temperature sensor + - xpath: './/product_dataline[@product_identifier="_0x2124"]' + node: 'resource_temperature' + unit_of_measurement: '°C' + # Humidity/temperature + - xpath: './/product_dataline[@product_identifier="_0x2135"]' + node: 'resource_humidity_level' + unit_of_measurement: '%' + # Humidity/temperature + - xpath: './/product_dataline[@product_identifier="_0x2135"]' + node: 'resource_temperature' + unit_of_measurement: '°C' + # Lux/temperature + - xpath: './/product_dataline[@product_identifier="_0x2136"]' + node: 'resource_light' + unit_of_measurement: 'Lux' + # Lux/temperature + - xpath: './/product_dataline[@product_identifier="_0x2136"]' + node: 'resource_temperature' + unit_of_measurement: '°C' + +switch: + # Wireless Plug outlet + - xpath: './/product_airlink[@product_identifier="_0x4201"]' + node: 'airlink_relay' + # Dataline universal relay + - xpath: './/product_airlink[@product_identifier="_0x4203"]' + node: 'airlink_relay' + # Dataline plug outlet + - xpath: './/product_dataline[@product_identifier="_0x2201"]' + node: 'dataline_output' + # Wireless mobile relay + - xpath: './/product_airlink[@product_identifier="_0x4204"]' + node: 'airlink_relay' + diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py new file mode 100644 index 00000000000..48827851f92 --- /dev/null +++ b/homeassistant/components/ihc/ihcdevice.py @@ -0,0 +1,65 @@ +"""Implements a base class for all IHC devices.""" +import asyncio +from xml.etree.ElementTree import Element + +from homeassistant.helpers.entity import Entity + + +class IHCDevice(Entity): + """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. + Derived classes must implement the on_ihc_change method + """ + + def __init__(self, ihc_controller, name, ihc_id: int, info: bool, + product: Element=None): + """Initialize IHC attributes.""" + self.ihc_controller = ihc_controller + self._name = name + self.ihc_id = ihc_id + self.info = info + if product: + self.ihc_name = product.attrib['name'] + self.ihc_note = product.attrib['note'] + self.ihc_position = product.attrib['position'] + else: + self.ihc_name = '' + self.ihc_note = '' + self.ihc_position = '' + + @asyncio.coroutine + def async_added_to_hass(self): + """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.""" + return False + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if not self.info: + return {} + return { + 'ihc_id': self.ihc_id, + 'ihc_name': self.ihc_name, + 'ihc_note': self.ihc_note, + 'ihc_position': self.ihc_position + } + + def on_ihc_change(self, ihc_id, value): + """Callback when ihc resource changes. + + Derived classes must overwrite this to do device specific stuff. + """ + raise NotImplementedError diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml new file mode 100644 index 00000000000..7b6053eff89 --- /dev/null +++ b/homeassistant/components/ihc/services.yaml @@ -0,0 +1,26 @@ +# Describes the format for available ihc services + +set_runtime_value_bool: + description: Set a boolean runtime value on the ihc controller + fields: + ihc_id: + description: The integer ihc resource id + value: + description: The boolean value to set + +set_runtime_value_int: + description: Set an integer runtime value on the ihc controller + fields: + ihc_id: + description: The integer ihc resource id + value: + description: The integer value to set + +set_runtime_value_float: + description: Set a float runtime value on the ihc controller + fields: + ihc_id: + description: The integer ihc resource id + value: + description: The float value to set + diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index d31d1e96431..82f98449411 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -5,7 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ from datetime import timedelta -from functools import wraps, partial +from functools import partial, wraps import logging import re @@ -13,13 +13,13 @@ import requests.exceptions import voluptuous as vol from homeassistant.const import ( - EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, CONF_HOST, - CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, - CONF_EXCLUDE, CONF_INCLUDE, CONF_DOMAINS, CONF_ENTITIES) + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, + CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues from homeassistant.util import utcnow -import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['influxdb==4.1.1'] @@ -227,6 +227,7 @@ def setup(hass, config): @RetryOnError(hass, retry_limit=max_tries, retry_delay=20, queue_limit=queue_limit) def _write_data(json_body): + """Write the data.""" try: influx.write_points(json_body) except exceptions.InfluxDBClientError: @@ -268,7 +269,7 @@ class RetryOnError(object): @wraps(method) def wrapper(*args, **kwargs): - """Wrapped method.""" + """Wrap method.""" # pylint: disable=protected-access if not hasattr(wrapper, "_retry_queue"): wrapper._retry_queue = [] diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local.py index dbe8597be3d..a18d4e0aa14 100644 --- a/homeassistant/components/insteon_local.py +++ b/homeassistant/components/insteon_local.py @@ -11,7 +11,7 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT) + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -37,14 +37,15 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) }, extra=vol.ALLOW_EXTRA) def setup(hass, config): - """Setup insteon hub.""" + """Set up the local Insteon hub.""" from insteonlocal.Hub import Hub + conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) @@ -62,20 +63,16 @@ def setup(hass, config): # Check for successful connection insteonhub.get_buffer_status() except requests.exceptions.ConnectTimeout: - _LOGGER.error( - "Could not connect. Check config", - exc_info=True) + _LOGGER.error("Could not connect", exc_info=True) return False except requests.exceptions.ConnectionError: - _LOGGER.error( - "Could not connect. Check config", - exc_info=True) + _LOGGER.error("Could not connect", exc_info=True) return False except requests.exceptions.RequestException: if insteonhub.http_code == 401: - _LOGGER.error("Bad user/pass for insteon_local hub") + _LOGGER.error("Bad username or password for Insteon_local hub") else: - _LOGGER.error("Error on insteon_local hub check", exc_info=True) + _LOGGER.error("Error on Insteon_local hub check", exc_info=True) return False linked = insteonhub.get_linked() diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py new file mode 100644 index 00000000000..237493c7919 --- /dev/null +++ b/homeassistant/components/iota.py @@ -0,0 +1,81 @@ +""" +Support for IOTA wallets. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/iota/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['pyota==2.0.3'] + +_LOGGER = logging.getLogger(__name__) + +CONF_IRI = 'iri' +CONF_TESTNET = 'testnet' +CONF_WALLET_NAME = 'name' +CONF_WALLET_SEED = 'seed' +CONF_WALLETS = 'wallets' + +DOMAIN = 'iota' + +IOTA_PLATFORMS = ['sensor'] + +SCAN_INTERVAL = timedelta(minutes=10) + +WALLET_CONFIG = vol.Schema({ + vol.Required(CONF_WALLET_NAME): cv.string, + vol.Required(CONF_WALLET_SEED): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_IRI): cv.string, + vol.Optional(CONF_TESTNET, default=False): cv.boolean, + vol.Required(CONF_WALLETS): vol.All(cv.ensure_list, [WALLET_CONFIG]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the IOTA component.""" + iota_config = config[DOMAIN] + + for platform in IOTA_PLATFORMS: + load_platform(hass, platform, DOMAIN, iota_config, config) + + return True + + +class IotaDevice(Entity): + """Representation of a IOTA device.""" + + def __init__(self, name, seed, iri, is_testnet=False): + """Initialisation of the IOTA device.""" + self._name = name + self._seed = seed + self.iri = iri + self.is_testnet = is_testnet + + @property + def name(self): + """Return the default name of the device.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {CONF_WALLET_NAME: self._name} + return attr + + @property + def api(self): + """Construct API object for interaction with the IRI node.""" + from iota import Iota + return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/kira.py b/homeassistant/components/kira.py index 98d1228d541..3a5ee25f05e 100644 --- a/homeassistant/components/kira.py +++ b/homeassistant/components/kira.py @@ -1,26 +1,23 @@ -"""KIRA interface to receive UDP packets from an IR-IP bridge.""" -# pylint: disable=import-error +""" +KIRA interface to receive UDP packets from an IR-IP bridge. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/kira/ +""" import logging import os -import yaml import voluptuous as vol from voluptuous.error import Error as VoluptuousError +import yaml +from homeassistant.const import ( + CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_TYPE, + EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_DEVICE, - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SENSORS, - CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN) - -REQUIREMENTS = ["pykira==0.1.1"] +REQUIREMENTS = ['pykira==0.1.1'] DOMAIN = 'kira' @@ -67,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema({ def load_codes(path): - """Load Kira codes from specified file.""" + """Load KIRA codes from specified file.""" codes = [] if os.path.exists(path): with open(path) as code_file: @@ -77,7 +74,7 @@ def load_codes(path): codes.append(CODE_SCHEMA(code)) except VoluptuousError as exception: # keep going - _LOGGER.warning('Kira Code Invalid Data: %s', exception) + _LOGGER.warning("KIRA code invalid data: %s", exception) else: with open(path, 'w') as code_file: code_file.write('') @@ -85,7 +82,7 @@ def load_codes(path): def setup(hass, config): - """Setup KIRA capability.""" + """Set up the KIRA component.""" import pykira sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) @@ -99,10 +96,10 @@ def setup(hass, config): hass.data[DOMAIN] = { CONF_SENSOR: {}, CONF_REMOTE: {}, - } + } def load_module(platform, idx, module_conf): - """Set up Kira module and load platform.""" + """Set up the KIRA module and load platform.""" # note: module_name is not the HA device name. it's just a unique name # to ensure the component and platform can share information module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN @@ -133,6 +130,7 @@ def setup(hass, config): load_module(CONF_REMOTE, idx, module_conf) def _stop_kira(_event): + """Stop the KIRA receiver.""" for receiver in hass.data[DOMAIN][CONF_SENSOR].values(): receiver.stop() _LOGGER.info("Terminated receivers") diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index f9747351bdd..eb5ae9a4590 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -1,22 +1,21 @@ """ - Connects to KNX platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/knx/ - """ -import logging import asyncio +import logging import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - CONF_HOST, CONF_PORT from homeassistant.helpers.script import Script +REQUIREMENTS = ['xknx==0.7.18'] + DOMAIN = "knx" DATA_KNX = "data_knx" CONF_KNX_CONFIG = "config_file" @@ -36,12 +35,10 @@ ATTR_DISCOVER_DEVICES = 'devices' _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['xknx==0.7.18'] - TUNNELING_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, vol.Required(CONF_KNX_LOCAL_IP): cv.string, + vol.Optional(CONF_PORT): cv.port, }) ROUTING_SCHEMA = vol.Schema({ @@ -57,9 +54,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Inclusive(CONF_KNX_FIRE_EVENT, 'fire_ev'): cv.boolean, vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): - vol.All( - cv.ensure_list, - [cv.string]), + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) @@ -73,7 +68,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): - """Set up knx component.""" + """Set up the KNX component.""" from xknx.exceptions import XKNXException try: hass.data[DATA_KNX] = KNXModule(hass, config) @@ -109,6 +104,7 @@ def async_setup(hass, config): def _get_devices(hass, discovery_type): + """Get the KNX devices.""" return list( map(lambda device: device.name, filter( @@ -120,7 +116,7 @@ class KNXModule(object): """Representation of KNX Object.""" def __init__(self, hass, config): - """Initialization of KNXModule.""" + """Initialize of KNX module.""" self.hass = hass self.config = config self.connected = False @@ -129,11 +125,9 @@ class KNXModule(object): self.register_callbacks() def init_xknx(self): - """Initialization of KNX object.""" + """Initialize of KNX object.""" from xknx import XKNX - self.xknx = XKNX( - config=self.config_file(), - loop=self.hass.loop) + self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop) @asyncio.coroutine def start(self): @@ -189,10 +183,8 @@ class KNXModule(object): if gateway_port is None: gateway_port = DEFAULT_MCAST_PORT return ConnectionConfig( - connection_type=ConnectionType.TUNNELING, - gateway_ip=gateway_ip, - gateway_port=gateway_port, - local_ip=local_ip) + connection_type=ConnectionType.TUNNELING, gateway_ip=gateway_ip, + gateway_port=gateway_port, local_ip=local_ip) def connection_config_auto(self): """Return the connection_config if auto is configured.""" @@ -213,7 +205,7 @@ class KNXModule(object): @asyncio.coroutine def telegram_received_cb(self, telegram): - """Callback invoked after a KNX telegram was received.""" + """Call invoked after a KNX telegram was received.""" self.hass.bus.fire('knx_event', { 'address': telegram.group_address.str(), 'data': telegram.payload.value @@ -254,8 +246,6 @@ class KNXAutomation(): import xknx self.action = xknx.devices.ActionCallback( - hass.data[DATA_KNX].xknx, - self.script.async_run, - hook=hook, - counter=counter) + hass.data[DATA_KNX].xknx, self.script.async_run, + hook=hook, counter=counter) device.actions.append(self.action) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 3d333e229fa..b761b04c705 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -5,33 +5,33 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light/ """ import asyncio +import csv from datetime import timedelta import logging import os -import csv import voluptuous as vol -from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.components import group from homeassistant.const import ( - STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_ENTITY_ID) + ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_ON) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass import homeassistant.util.color as color_util -DOMAIN = "light" +DOMAIN = 'light' DEPENDENCIES = ['group'] SCAN_INTERVAL = timedelta(seconds=30) GROUP_NAME_ALL_LIGHTS = 'all lights' ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights') -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = DOMAIN + '.{}' # Bitfield of features supported by the light entity SUPPORT_BRIGHTNESS = 1 @@ -220,7 +220,7 @@ def toggle(hass, entity_id=None, transition=None): def preprocess_turn_on_alternatives(params): - """Processing extra data for turn light on request.""" + """Process extra data for turn light on request.""" profile = Profiles.get(params.pop(ATTR_PROFILE, None)) if profile is not None: params.setdefault(ATTR_XY_COLOR, profile[:2]) @@ -242,7 +242,7 @@ def preprocess_turn_on_alternatives(params): @asyncio.coroutine def async_setup(hass, config): - """Expose light control via statemachine and services.""" + """Expose light control via state machine and services.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) yield from component.async_setup(config) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a1c43ad4cbc..6b22190dce9 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,15 +4,14 @@ Support for deCONZ light. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ - import asyncio from homeassistant.components.deconz import DOMAIN as DECONZ_DATA from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_FLASH, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_RGB_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR) + SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) from homeassistant.core import callback from homeassistant.util.color import color_RGB_to_xy @@ -23,7 +22,7 @@ ATTR_LIGHT_GROUP = 'LightGroup' @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup light for deCONZ component.""" + """Set up the deCONZ light.""" if discovery_info is None: return @@ -44,7 +43,7 @@ class DeconzLight(Light): """Representation of a deCONZ light.""" def __init__(self, light): - """Setup light and add update callback to get data from websocket.""" + """Set up light and add update callback to get data from websocket.""" self._light = light self._features = SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index 17cc741c593..6d502e15d6f 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['decora==0.6', 'bluepy==1.1.1'] +REQUIREMENTS = ['decora==0.6', 'bluepy==1.1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 95f13cad860..c48de4deaf8 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['flux_led==0.20'] +REQUIREMENTS = ['flux_led==0.21'] _LOGGER = logging.getLogger(__name__) @@ -78,7 +78,7 @@ EFFECT_MAP = { FLUX_EFFECT_LIST = [ EFFECT_RANDOM, - ].extend(EFFECT_MAP.keys()) + ] + list(EFFECT_MAP) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py index 0e99a49eaa9..5ad7fd4c317 100644 --- a/homeassistant/components/light/greenwave.py +++ b/homeassistant/components/light/greenwave.py @@ -5,60 +5,72 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.greenwave/ """ import logging +from datetime import timedelta import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS) + ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle -SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS) - -REQUIREMENTS = ['greenwavereality==0.2.9'] +REQUIREMENTS = ['greenwavereality==0.5.1'] _LOGGER = logging.getLogger(__name__) +CONF_VERSION = 'version' + +SUPPORTED_FEATURES = SUPPORT_BRIGHTNESS + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Required("version"): cv.positive_int, + vol.Required(CONF_VERSION): cv.positive_int, }) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Greenwave Reality Platform.""" + """Set up the Greenwave Reality Platform.""" import greenwavereality as greenwave import os host = config.get(CONF_HOST) tokenfile = hass.config.path('.greenwave') - if config.get("version") == 3: + if config.get(CONF_VERSION) == 3: if os.path.exists(tokenfile): tokenfile = open(tokenfile) token = tokenfile.read() tokenfile.close() else: - token = greenwave.grab_token(host, 'hass', 'homeassistant') + try: + token = greenwave.grab_token(host, 'hass', 'homeassistant') + except PermissionError: + _LOGGER.error('The Gateway Is Not In Sync Mode') + raise tokenfile = open(tokenfile, "w+") tokenfile.write(token) tokenfile.close() else: token = None - doc = greenwave.grab_xml(host, token) - add_devices(GreenwaveLight(device, host, token) for device in doc) + bulbs = greenwave.grab_bulbs(host, token) + add_devices(GreenwaveLight(device, host, token, GatewayData(host, token)) + for device in bulbs.values()) class GreenwaveLight(Light): """Representation of an Greenwave Reality Light.""" - def __init__(self, light, host, token): + def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" import greenwavereality as greenwave - self._did = light['did'] + self._did = int(light['did']) self._name = light['name'] self._state = int(light['state']) self._brightness = greenwave.hass_brightness(light) self._host = host self._online = greenwave.check_online(light) - self.token = token + self._token = token + self._gatewaydata = gatewaydata @property def supported_features(self): @@ -91,22 +103,44 @@ class GreenwaveLight(Light): temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, - temp_brightness, self.token) - greenwave.turn_on(self._host, self._did, self.token) + temp_brightness, self._token) + greenwave.turn_on(self._host, self._did, self._token) def turn_off(self, **kwargs): """Instruct the light to turn off.""" import greenwavereality as greenwave - greenwave.turn_off(self._host, self._did, self.token) + greenwave.turn_off(self._host, self._did, self._token) def update(self): """Fetch new state data for this light.""" import greenwavereality as greenwave - doc = greenwave.grab_xml(self._host, self.token) + self._gatewaydata.update() + bulbs = self._gatewaydata.greenwave - for device in doc: - if device['did'] == self._did: - self._state = int(device['state']) - self._brightness = greenwave.hass_brightness(device) - self._online = greenwave.check_online(device) - self._name = device['name'] + self._state = int(bulbs[self._did]['state']) + self._brightness = greenwave.hass_brightness(bulbs[self._did]) + self._online = greenwave.check_online(bulbs[self._did]) + self._name = bulbs[self._did]['name'] + + +class GatewayData(object): + """Handle Gateway data and limit updates.""" + + def __init__(self, host, token): + """Initialize the data object.""" + import greenwavereality as greenwave + self._host = host + self._token = token + self._greenwave = greenwave.grab_bulbs(host, token) + + @property + def greenwave(self): + """Return Gateway API object.""" + return self._greenwave + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from the gateway.""" + import greenwavereality as greenwave + self._greenwave = greenwave.grab_bulbs(self._host, self._token) + return self._greenwave diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index 3356d637be8..8fafb88a7db 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -1,141 +1,141 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.hive/ -""" -import colorsys -from homeassistant.components.hive import DATA_HIVE -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) - -DEPENDENCIES = ['hive'] - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Hive light devices.""" - if discovery_info is None: - return - session = hass.data.get(DATA_HIVE) - - add_devices([HiveDeviceLight(session, discovery_info)]) - - -class HiveDeviceLight(Light): - """Hive Active Light Device.""" - - def __init__(self, hivesession, hivedevice): - """Initialize the Light device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.light_device_type = hivedevice["Hive_Light_DeviceType"] - self.session = hivesession - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) - self.session.entities.append(self) - - def handle_update(self, updatesource): - """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: - self.schedule_update_ha_state() - - @property - def name(self): - """Return the display name of this light.""" - return self.node_name - - @property - def brightness(self): - """Brightness of the light (an integer in the range 1-255).""" - return self.session.light.get_brightness(self.node_id) - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": - return self.session.light.get_min_color_temp(self.node_id) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": - return self.session.light.get_max_color_temp(self.node_id) - - @property - def color_temp(self): - """Return the CT color value in mireds.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": - return self.session.light.get_color_temp(self.node_id) - - @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" - if self.light_device_type == "colourtuneablelight": - return self.session.light.get_color(self.node_id) - - @property - def is_on(self): - """Return true if light is on.""" - return self.session.light.get_state(self.node_id) - - def turn_on(self, **kwargs): - """Instruct the light to turn on.""" - new_brightness = None - new_color_temp = None - new_color = None - if ATTR_BRIGHTNESS in kwargs: - tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) - percentage_brightness = ((tmp_new_brightness / 255) * 100) - new_brightness = int(round(percentage_brightness / 5.0) * 5.0) - if new_brightness == 0: - new_brightness = 5 - if ATTR_COLOR_TEMP in kwargs: - tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) - new_color_temp = round(1000000 / tmp_new_color_temp) - if ATTR_RGB_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_RGB_COLOR) - tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], - get_new_color[1], - get_new_color[2]) - hue = int(round(tmp_new_color[0] * 360)) - saturation = int(round(tmp_new_color[1] * 100)) - value = int(round((tmp_new_color[2] / 255) * 100)) - new_color = (hue, saturation, value) - - self.session.light.turn_on(self.node_id, self.light_device_type, - new_brightness, new_color_temp, - new_color) - - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def turn_off(self): - """Instruct the light to turn off.""" - self.session.light.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = None - if self.light_device_type == "warmwhitelight": - supported_features = SUPPORT_BRIGHTNESS - elif self.light_device_type == "tuneablelight": - supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) - elif self.light_device_type == "colourtuneablelight": - supported_features = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR) - - return supported_features - - def update(self): - """Update all Node data frome Hive.""" - self.session.core.update_data(self.node_id) +""" +Support for the Hive devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.hive/ +""" +import colorsys +from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + SUPPORT_RGB_COLOR, Light) + +DEPENDENCIES = ['hive'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Hive light devices.""" + if discovery_info is None: + return + session = hass.data.get(DATA_HIVE) + + add_devices([HiveDeviceLight(session, discovery_info)]) + + +class HiveDeviceLight(Light): + """Hive Active Light Device.""" + + def __init__(self, hivesession, hivedevice): + """Initialize the Light device.""" + self.node_id = hivedevice["Hive_NodeID"] + self.node_name = hivedevice["Hive_NodeName"] + self.device_type = hivedevice["HA_DeviceType"] + self.light_device_type = hivedevice["Hive_Light_DeviceType"] + self.session = hivesession + self.data_updatesource = '{}.{}'.format(self.device_type, + self.node_id) + self.session.entities.append(self) + + def handle_update(self, updatesource): + """Handle the new update request.""" + if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + self.schedule_update_ha_state() + + @property + def name(self): + """Return the display name of this light.""" + return self.node_name + + @property + def brightness(self): + """Brightness of the light (an integer in the range 1-255).""" + return self.session.light.get_brightness(self.node_id) + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + if self.light_device_type == "tuneablelight" \ + or self.light_device_type == "colourtuneablelight": + return self.session.light.get_min_color_temp(self.node_id) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + if self.light_device_type == "tuneablelight" \ + or self.light_device_type == "colourtuneablelight": + return self.session.light.get_max_color_temp(self.node_id) + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + if self.light_device_type == "tuneablelight" \ + or self.light_device_type == "colourtuneablelight": + return self.session.light.get_color_temp(self.node_id) + + @property + def rgb_color(self) -> tuple: + """Return the RBG color value.""" + if self.light_device_type == "colourtuneablelight": + return self.session.light.get_color(self.node_id) + + @property + def is_on(self): + """Return true if light is on.""" + return self.session.light.get_state(self.node_id) + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + new_brightness = None + new_color_temp = None + new_color = None + if ATTR_BRIGHTNESS in kwargs: + tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) + percentage_brightness = ((tmp_new_brightness / 255) * 100) + new_brightness = int(round(percentage_brightness / 5.0) * 5.0) + if new_brightness == 0: + new_brightness = 5 + if ATTR_COLOR_TEMP in kwargs: + tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) + new_color_temp = round(1000000 / tmp_new_color_temp) + if ATTR_RGB_COLOR in kwargs: + get_new_color = kwargs.get(ATTR_RGB_COLOR) + tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], + get_new_color[1], + get_new_color[2]) + hue = int(round(tmp_new_color[0] * 360)) + saturation = int(round(tmp_new_color[1] * 100)) + value = int(round((tmp_new_color[2] / 255) * 100)) + new_color = (hue, saturation, value) + + self.session.light.turn_on(self.node_id, self.light_device_type, + new_brightness, new_color_temp, + new_color) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + def turn_off(self): + """Instruct the light to turn off.""" + self.session.light.turn_off(self.node_id) + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = None + if self.light_device_type == "warmwhitelight": + supported_features = SUPPORT_BRIGHTNESS + elif self.light_device_type == "tuneablelight": + supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) + elif self.light_device_type == "colourtuneablelight": + supported_features = ( + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR) + + return supported_features + + def update(self): + """Update all Node data frome Hive.""" + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index f4ea04240f1..cbabaafd3fb 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -14,13 +14,12 @@ import socket import voluptuous as vol import homeassistant.components.hue as hue - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, - SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA) + FLASH_LONG, FLASH_SHORT, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, + SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util as util @@ -114,7 +113,7 @@ def update_lights(hass, bridge, add_devices): def unthrottled_update_lights(hass, bridge, add_devices): - """Internal version of update_lights.""" + """Update the lights (Internal version of update_lights).""" import phue if not bridge.configured: @@ -123,14 +122,14 @@ def unthrottled_update_lights(hass, bridge, add_devices): try: api = bridge.get_api() except phue.PhueRequestTimeout: - _LOGGER.warning('Timeout trying to reach the bridge') + _LOGGER.warning("Timeout trying to reach the bridge") return except ConnectionRefusedError: - _LOGGER.error('The bridge refused the connection') + _LOGGER.error("The bridge refused the connection") return except socket.error: # socket.error when we cannot reach Hue - _LOGGER.exception('Cannot reach the bridge') + _LOGGER.exception("Cannot reach the bridge") return new_lights = process_lights( @@ -151,7 +150,7 @@ def process_lights(hass, api, bridge, update_lights_cb): api_lights = api.get('lights') if not isinstance(api_lights, dict): - _LOGGER.error('Got unexpected result from Hue API') + _LOGGER.error("Got unexpected result from Hue API") return [] new_lights = [] @@ -186,8 +185,8 @@ def process_groups(hass, api, bridge, update_lights_cb): for lightgroup_id, info in api_groups.items(): if 'state' not in info: - _LOGGER.warning('Group info does not contain state. ' - 'Please update your hub.') + _LOGGER.warning( + "Group info does not contain state. Please update your hub") return [] if lightgroup_id not in bridge.lightgroups: diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 88bdc1a4c95..4701866cd9a 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -11,7 +11,8 @@ import socket import voluptuous as vol from homeassistant.components.light import ( - ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_RGB_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -19,13 +20,27 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_COLOR = 'default_color' CONF_PRIORITY = 'priority' +CONF_HDMI_PRIORITY = 'hdmi_priority' +CONF_EFFECT_LIST = 'effect_list' DEFAULT_COLOR = [255, 255, 255] DEFAULT_NAME = 'Hyperion' DEFAULT_PORT = 19444 DEFAULT_PRIORITY = 128 +DEFAULT_HDMI_PRIORITY = 880 +DEFAULT_EFFECT_LIST = ['HDMI', 'Cinema brighten lights', 'Cinema dim lights', + 'Knight rider', 'Blue mood blobs', 'Cold mood blobs', + 'Full color mood blobs', 'Green mood blobs', + 'Red mood blobs', 'Warm mood blobs', + 'Police Lights Single', 'Police Lights Solid', + 'Rainbow mood', 'Rainbow swirl fast', + 'Rainbow swirl', 'Random', 'Running dots', + 'System Shutdown', 'Snake', 'Sparks Color', 'Sparks', + 'Strobe blue', 'Strobe Raspbmc', 'Strobe white', + 'Color traces', 'UDP multicast listener', + 'UDP listener', 'X-Mas'] -SUPPORT_HYPERION = SUPPORT_RGB_COLOR +SUPPORT_HYPERION = (SUPPORT_RGB_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -35,6 +50,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int, + vol.Optional(CONF_HDMI_PRIORITY, + default=DEFAULT_HDMI_PRIORITY): cv.positive_int, + vol.Optional(CONF_EFFECT_LIST, + default=DEFAULT_EFFECT_LIST): vol.All(cv.ensure_list, + [cv.string]), }) @@ -43,10 +63,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) priority = config.get(CONF_PRIORITY) + hdmi_priority = config.get(CONF_HDMI_PRIORITY) default_color = config.get(CONF_DEFAULT_COLOR) + effect_list = config.get(CONF_EFFECT_LIST) device = Hyperion(config.get(CONF_NAME), host, port, priority, - default_color) + default_color, hdmi_priority, effect_list) if device.setup(): add_devices([device]) @@ -57,20 +79,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class Hyperion(Light): """Representation of a Hyperion remote.""" - def __init__(self, name, host, port, priority, default_color): + def __init__(self, name, host, port, priority, default_color, + hdmi_priority, effect_list): """Initialize the light.""" self._host = host self._port = port self._name = name self._priority = priority + self._hdmi_priority = hdmi_priority self._default_color = default_color self._rgb_color = [0, 0, 0] + self._rgb_mem = [0, 0, 0] + self._brightness = 255 + self._icon = 'mdi:lightbulb' + self._effect_list = effect_list + self._effect = None + self._skip_update = False @property def name(self): """Return the name of the light.""" return self._name + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + @property def rgb_color(self): """Return last RGB color value set.""" @@ -81,6 +116,21 @@ class Hyperion(Light): """Return true if not black.""" return self._rgb_color != [0, 0, 0] + @property + def icon(self): + """Return state specific icon.""" + return self._icon + + @property + def effect(self): + """Return the current effect.""" + return self._effect + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + @property def supported_features(self): """Flag supported features.""" @@ -89,35 +139,106 @@ class Hyperion(Light): def turn_on(self, **kwargs): """Turn the lights on.""" if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + rgb_color = kwargs[ATTR_RGB_COLOR] + elif self._rgb_mem == [0, 0, 0]: + rgb_color = self._default_color else: - self._rgb_color = self._default_color + rgb_color = self._rgb_mem + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + else: + brightness = self._brightness + + if ATTR_EFFECT in kwargs: + self._skip_update = True + self._effect = kwargs[ATTR_EFFECT] + if self._effect == 'HDMI': + self.json_request({'command': 'clearall'}) + self._icon = 'mdi:video-input-hdmi' + self._brightness = 255 + self._rgb_color = [125, 125, 125] + else: + self.json_request({ + 'command': 'effect', + 'priority': self._priority, + 'effect': {'name': self._effect} + }) + self._icon = 'mdi:lava-lamp' + self._rgb_color = [175, 0, 255] + return + + cal_color = [int(round(x*float(brightness)/255)) + for x in rgb_color] self.json_request({ 'command': 'color', 'priority': self._priority, - 'color': self._rgb_color + 'color': cal_color }) def turn_off(self, **kwargs): """Disconnect all remotes.""" self.json_request({'command': 'clearall'}) - self._rgb_color = [0, 0, 0] + self.json_request({ + 'command': 'color', + 'priority': self._priority, + 'color': [0, 0, 0] + }) def update(self): - """Get the remote's active color.""" + """Get the lights status.""" + # postpone the immediate state check for changes that take time + if self._skip_update: + self._skip_update = False + return response = self.json_request({'command': 'serverinfo'}) if response: # workaround for outdated Hyperion if 'activeLedColor' not in response['info']: self._rgb_color = self._default_color + self._rgb_mem = self._default_color + self._brightness = 255 + self._icon = 'mdi:lightbulb' + self._effect = None return + # Check if Hyperion is in ambilight mode trough an HDMI grabber + try: + active_priority = response['info']['priorities'][0]['priority'] + if active_priority == self._hdmi_priority: + self._brightness = 255 + self._rgb_color = [125, 125, 125] + self._icon = 'mdi:video-input-hdmi' + self._effect = 'HDMI' + return + except (KeyError, IndexError): + pass - if response['info']['activeLedColor'] == []: - self._rgb_color = [0, 0, 0] + if not response['info']['activeLedColor']: + # Get the active effect + if response['info']['activeEffects']: + self._rgb_color = [175, 0, 255] + self._icon = 'mdi:lava-lamp' + try: + s_name = response['info']['activeEffects'][0]["script"] + s_name = s_name.split('/')[-1][:-3].split("-")[0] + self._effect = [x for x in self._effect_list + if s_name.lower() in x.lower()][0] + except (KeyError, IndexError): + self._effect = None + # Bulb off state + else: + self._rgb_color = [0, 0, 0] + self._icon = 'mdi:lightbulb' + self._effect = None else: + # Get the RGB color self._rgb_color =\ response['info']['activeLedColor'][0]['RGB Value'] + self._brightness = max(self._rgb_color) + self._rgb_mem = [int(round(float(x)*255/self._brightness)) + for x in self._rgb_color] + self._icon = 'mdi:lightbulb' + self._effect = None def setup(self): """Get the hostname of the remote.""" diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 11366ffc45c..a2eed36a089 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -9,13 +9,10 @@ import math import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - Light, PLATFORM_SCHEMA -) - + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py new file mode 100644 index 00000000000..f23ae77c8b2 --- /dev/null +++ b/homeassistant/components/light/ihc.py @@ -0,0 +1,123 @@ +"""IHC light platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.ihc/ +""" +from xml.etree.ElementTree import Element + +import voluptuous as vol + +from homeassistant.components.ihc import ( + validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO) +from homeassistant.components.ihc.const import CONF_DIMMABLE +from homeassistant.components.ihc.ihcdevice import IHCDevice +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA, Light) +from homeassistant.const import CONF_ID, CONF_NAME, CONF_LIGHTS +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ihc'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_LIGHTS, default=[]): + vol.All(cv.ensure_list, [ + vol.All({ + vol.Required(CONF_ID): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_DIMMABLE, default=False): cv.boolean, + }, validate_name) + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ihc lights platform.""" + ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER] + info = hass.data[IHC_DATA][IHC_INFO] + devices = [] + if discovery_info: + for name, device in discovery_info.items(): + ihc_id = device['ihc_id'] + product_cfg = device['product_cfg'] + product = device['product'] + light = IhcLight(ihc_controller, name, ihc_id, info, + product_cfg[CONF_DIMMABLE], product) + devices.append(light) + else: + lights = config[CONF_LIGHTS] + for light in lights: + ihc_id = light[CONF_ID] + name = light[CONF_NAME] + dimmable = light[CONF_DIMMABLE] + device = IhcLight(ihc_controller, name, ihc_id, info, dimmable) + devices.append(device) + + add_devices(devices) + + +class IhcLight(IHCDevice, Light): + """Representation of a IHC light. + + For dimmable lights, the associated IHC resource should be a light + level (integer). For non dimmable light the IHC resource should be + an on/off (boolean) resource + """ + + def __init__(self, ihc_controller, name, ihc_id: int, info: bool, + dimmable=False, product: Element=None): + """Initialize the light.""" + super().__init__(ihc_controller, name, ihc_id, info, product) + self._brightness = 0 + self._dimmable = dimmable + self._state = None + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Flag supported features.""" + if self._dimmable: + return SUPPORT_BRIGHTNESS + return 0 + + def turn_on(self, **kwargs) -> None: + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + else: + brightness = self._brightness + if brightness == 0: + brightness = 255 + + if self._dimmable: + self.ihc_controller.set_runtime_value_int( + self.ihc_id, int(brightness * 100 / 255)) + else: + self.ihc_controller.set_runtime_value_bool(self.ihc_id, True) + + def turn_off(self, **kwargs) -> None: + """Turn the light off.""" + if self._dimmable: + self.ihc_controller.set_runtime_value_int(self.ihc_id, 0) + else: + self.ihc_controller.set_runtime_value_bool(self.ihc_id, False) + + def on_ihc_change(self, ihc_id, value): + """Callback from Ihc notifications.""" + if isinstance(value, bool): + self._dimmable = False + self._state = value != 0 + else: + self._dimmable = True + self._state = value > 0 + if self._state: + self._brightness = int(value * 255 / 100) + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index c1caf91db45..732cfe2a644 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -5,11 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.knx/ """ import asyncio + import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES -from homeassistant.components.light import PLATFORM_SCHEMA, Light, \ - SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -32,20 +33,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): - """Set up light(s) for KNX platform.""" +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up lights for KNX platform.""" if DATA_KNX not in hass.data \ or not hass.data[DATA_KNX].initialized: - return False + return if discovery_info is not None: async_add_devices_discovery(hass, discovery_info, async_add_devices) else: async_add_devices_config(hass, config, async_add_devices) - return True - @callback def async_add_devices_discovery(hass, discovery_info, async_add_devices): @@ -77,7 +75,7 @@ class KNXLight(Light): """Representation of a KNX light.""" def __init__(self, hass, device): - """Initialization of KNXLight.""" + """Initialize of KNX light.""" self.device = device self.hass = hass self.async_register_callbacks() @@ -87,7 +85,7 @@ class KNXLight(Light): """Register callbacks to update hass after device was changed.""" @asyncio.coroutine def after_update_callback(device): - """Callback after device was updated.""" + """Call after device was updated.""" # pylint: disable=unused-argument yield from self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 22ec58f65cd..090341e4255 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -4,29 +4,28 @@ Support for the LIFX platform that implements lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ -import logging import asyncio -import sys -import math -from functools import partial from datetime import timedelta +from functools import partial +import logging +import math +import sys import voluptuous as vol +from homeassistant import util from homeassistant.components.light import ( - Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA, - ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR, - ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, - VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, + DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, + SUPPORT_XY_COLOR, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant import util from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.service import extract_entity_ids -import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -296,12 +295,12 @@ class LIFXManager(object): @callback def register(self, device): - """Handler for newly detected bulb.""" + """Handle newly detected bulb.""" self.hass.async_add_job(self.async_register(device)) @asyncio.coroutine def async_register(self, device): - """Handler for newly detected bulb.""" + """Handle newly detected bulb.""" if device.mac_addr in self.entities: entity = self.entities[device.mac_addr] entity.registered = True diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 3646de977cf..19747b89ca0 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -17,6 +17,7 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) +from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) @@ -42,6 +43,7 @@ DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_WHITE_VALUE = False DEFAULT_XY = False +DEFAULT_BRIGHTNESS_SCALE = 255 CONF_EFFECT_LIST = 'effect_list' @@ -51,6 +53,8 @@ CONF_FLASH_TIME_SHORT = 'flash_time_short' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, + vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE): + vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), @@ -102,7 +106,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_BRIGHTNESS_SCALE) )]) @@ -112,7 +117,7 @@ class MqttJson(MqttAvailability, Light): def __init__(self, name, effect_list, topic, qos, retain, optimistic, brightness, color_temp, effect, rgb, white_value, xy, flash_times, availability_topic, payload_available, - payload_not_available): + payload_not_available, brightness_scale): """Initialize MQTT JSON light.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -154,6 +159,7 @@ class MqttJson(MqttAvailability, Light): self._xy = None self._flash_times = flash_times + self._brightness_scale = brightness_scale self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) self._supported_features |= (rgb and SUPPORT_RGB_COLOR) @@ -192,7 +198,9 @@ class MqttJson(MqttAvailability, Light): if self._brightness is not None: try: - self._brightness = int(values['brightness']) + self._brightness = int(values['brightness'] / + float(self._brightness_scale) * + 255) except KeyError: pass except ValueError: @@ -333,7 +341,9 @@ class MqttJson(MqttAvailability, Light): message['transition'] = int(kwargs[ATTR_TRANSITION]) if ATTR_BRIGHTNESS in kwargs: - message['brightness'] = int(kwargs[ATTR_BRIGHTNESS]) + message['brightness'] = int(kwargs[ATTR_BRIGHTNESS] / + float(DEFAULT_BRIGHTNESS_SCALE) * + self._brightness_scale) if self._optimistic: self._brightness = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index c41f480c67e..9a48b13ed3b 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -16,7 +16,7 @@ SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the mysensors platform for lights.""" + """Set up the MySensors platform for lights.""" device_class_map = { 'S_DIMMER': MySensorsLightDimmer, 'S_RGB_LIGHT': MySensorsLightRGB, diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 5785f0f1fc7..ff526c4783d 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -63,7 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): setup_bridge(bridge, add_devices, add_groups) -def setup_bridge(bridge, add_devices_callback, add_groups): +def setup_bridge(bridge, add_devices, add_groups): """Set up the Lightify bridge.""" lights = {} @@ -100,7 +100,7 @@ def setup_bridge(bridge, add_devices_callback, add_groups): lights[group_name].group = group if new_lights: - add_devices_callback(new_lights) + add_devices(new_lights) update_lights() @@ -109,7 +109,7 @@ class Luminary(Light): """Representation of Luminary Lights and Groups.""" def __init__(self, luminary, update_lights): - """Initize a Luminary light.""" + """Initialize a Luminary light.""" self.update_lights = update_lights self._luminary = luminary self._brightness = None diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 23814f16598..0bcf6933e68 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -159,3 +159,13 @@ lifx_effect_stop: entity_id: description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. example: 'light.bedroom' + +xiaomi_miio_set_scene: + description: Set a fixed scene. + fields: + entity_id: + description: Name of the light entity. + example: 'light.xiaomi_miio' + scene: + description: Number of the fixed scene, between 1 and 4. + example: 1 diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 87004f45ea0..30ad3a4d268 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -78,9 +78,7 @@ class TPLinkSmartBulb(Light): def __init__(self, smartbulb: 'SmartBulb', name): """Initialize the bulb.""" self.smartbulb = smartbulb - self._name = None - if name is not None: - self._name = name + self._name = name self._state = None self._available = True self._color_temp = None @@ -149,22 +147,30 @@ class TPLinkSmartBulb(Light): from pyHS100 import SmartDeviceException try: self._available = True + if self._supported_features == 0: self.get_features() + self._state = ( self.smartbulb.state == self.smartbulb.BULB_STATE_ON) - if self._name is None: + + # Pull the name from the device if a name was not specified + if self._name == DEFAULT_NAME: self._name = self.smartbulb.alias + if self._supported_features & SUPPORT_BRIGHTNESS: self._brightness = brightness_from_percentage( self.smartbulb.brightness) + if self._supported_features & SUPPORT_COLOR_TEMP: if (self.smartbulb.color_temp is not None and self.smartbulb.color_temp != 0): self._color_temp = kelvin_to_mired( self.smartbulb.color_temp) + if self._supported_features & SUPPORT_RGB_COLOR: self._rgb = hsv_to_rgb(self.smartbulb.hsv) + if self.smartbulb.has_emeter: self._emeter_params[ATTR_CURRENT_POWER_W] = '{:.1f}'.format( self.smartbulb.current_consumption()) diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 620271a1071..693e40c0292 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -4,6 +4,7 @@ Support for Belkin WeMo lights. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.wemo/ """ +import asyncio import logging from datetime import timedelta @@ -13,6 +14,7 @@ from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR) +from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] @@ -26,7 +28,7 @@ SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the WeMo bridges and register connected lights.""" + """Set up discovered WeMo switches.""" import pywemo.discovery as discovery if discovery_info is not None: @@ -34,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mac = discovery_info['mac_address'] device = discovery.device_from_description(location, mac) - if device: + if device.model_name == 'Dimmer': + add_devices([WemoDimmer(device)]) + else: setup_bridge(device, add_devices) @@ -140,3 +144,88 @@ class WemoLight(Light): def update(self): """Synchronize state with bridge.""" self.update_lights(no_throttle=True) + + +class WemoDimmer(Light): + """Representation of a WeMo dimmer.""" + + def __init__(self, device): + """Initialize the WeMo dimmer.""" + self.wemo = device + self._brightness = None + self._state = None + + @asyncio.coroutine + def async_added_to_hass(self): + """Register update callback.""" + wemo = get_component('wemo') + # The register method uses a threading condition, so call via executor. + # and yield from to wait until the task is done. + yield from self.hass.async_add_job( + wemo.SUBSCRIPTION_REGISTRY.register, self.wemo) + # The on method just appends to a defaultdict list. + wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) + + def _update_callback(self, _device, _type, _params): + """Update the state by the Wemo device.""" + _LOGGER.debug("Subscription update for %s", _device) + updated = self.wemo.subscription_update(_type, _params) + self._update(force_update=(not updated)) + self.schedule_update_ha_state() + + @property + def unique_id(self): + """Return the ID of this WeMo dimmer.""" + return "{}.{}".format(self.__class__, self.wemo.serialnumber) + + @property + def name(self): + """Return the name of the dimmer if any.""" + return self.wemo.name + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def should_poll(self): + """No polling needed with subscriptions.""" + return False + + @property + def brightness(self): + """Return the brightness of this light between 1 and 100.""" + return self._brightness + + @property + def is_on(self): + """Return true if dimmer is on. Standby is on.""" + return self._state + + def _update(self, force_update=True): + """Update the device state.""" + try: + self._state = self.wemo.get_state(force_update) + wemobrightness = int(self.wemo.get_brightness(force_update)) + self._brightness = int((wemobrightness * 255) / 100) + except AttributeError as err: + _LOGGER.warning("Could not update status for %s (%s)", + self.name, err) + + def turn_on(self, **kwargs): + """Turn the dimmer on.""" + self.wemo.on() + + # Wemo dimmer switches use a range of [0, 100] to control + # brightness. Level 255 might mean to set it to previous value + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + brightness = int((brightness / 255) * 100) + else: + brightness = 255 + self.wemo.set_brightness(brightness) + + def turn_off(self, **kwargs): + """Turn the dimmer off.""" + self.wemo.off() diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 445fe8ceb25..02605d24faf 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -10,7 +10,7 @@ import colorsys from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) -from homeassistant.components.wink import WinkDevice, DOMAIN +from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -39,7 +39,7 @@ class WinkLight(WinkDevice, Light): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['light'].append(self) @property diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index 63770fbf9b7..d1664d13072 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -39,7 +39,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): """Return true if it is on.""" return self._state - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) if value is None: diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index b35b5a3740e..43c8860e77b 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -2,7 +2,7 @@ Support for Xiaomi Philips Lights (LED Ball & Ceiling Lamp, Eyecare Lamp 2). For more details about this platform, please refer to the documentation -https://home-assistant.io/components/light.xiaomi_philipslight/ +https://home-assistant.io/components/light.xiaomi_miio/ """ import asyncio from functools import partial @@ -13,7 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, ) + ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, ATTR_ENTITY_ID, DOMAIN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) from homeassistant.exceptions import PlatformNotReady @@ -21,6 +21,7 @@ from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Philips Light' +PLATFORM = 'xiaomi_miio' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -28,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.3'] +REQUIREMENTS = ['python-miio==0.3.4'] # The light does not accept cct values < 1 CCT_MIN = 1 @@ -36,6 +37,24 @@ CCT_MAX = 100 SUCCESS = ['ok'] ATTR_MODEL = 'model' +ATTR_SCENE = 'scene' + +SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' + +XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SCHEMA_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_SCENE): + vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4)) +}) + +SERVICE_TO_METHOD = { + SERVICE_SET_SCENE: { + 'method': 'async_set_scene', + 'schema': SERVICE_SCHEMA_SCENE} +} # pylint: disable=unused-argument @@ -43,6 +62,8 @@ ATTR_MODEL = 'model' def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the light from config.""" from miio import Device, DeviceException + if PLATFORM not in hass.data: + hass.data[PLATFORM] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -50,7 +71,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - devices = [] try: light = Device(host, token) device_info = light.info() @@ -63,27 +83,53 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): from miio import PhilipsEyecare light = PhilipsEyecare(host, token) device = XiaomiPhilipsEyecareLamp(name, light, device_info) - devices.append(device) elif device_info.model == 'philips.light.ceiling': from miio import Ceil light = Ceil(host, token) device = XiaomiPhilipsCeilingLamp(name, light, device_info) - devices.append(device) elif device_info.model == 'philips.light.bulb': from miio import PhilipsBulb light = PhilipsBulb(host, token) device = XiaomiPhilipsLightBall(name, light, device_info) - devices.append(device) else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' 'https://github.com/rytilahti/python-miio/issues ' 'and provide the following data: %s', device_info.model) + return False except DeviceException: raise PlatformNotReady - async_add_devices(devices, update_before_add=True) + hass.data[PLATFORM][host] = device + async_add_devices([device], update_before_add=True) + + @asyncio.coroutine + def async_service_handler(service): + """Map services to methods on Xiaomi Philips Lights.""" + method = SERVICE_TO_METHOD.get(service.service) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + target_devices = [dev for dev in hass.data[PLATFORM].values() + if dev.entity_id in entity_ids] + else: + target_devices = hass.data[PLATFORM].values() + + update_tasks = [] + for target_device in target_devices: + yield from getattr(target_device, method['method'])(**params) + update_tasks.append(target_device.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + for xiaomi_miio_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( + 'schema', XIAOMI_MIIO_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema) class XiaomiPhilipsGenericLight(Light): @@ -194,6 +240,13 @@ class XiaomiPhilipsGenericLight(Light): except DeviceException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) + @asyncio.coroutine + def async_set_scene(self, scene: int=1): + """Set the fixed scene.""" + yield from self._try_command( + "Setting a fixed scene failed.", + self._light.set_scene, scene) + @staticmethod def translate(value, left_min, left_max, right_min, right_max): """Map a value from left span to right span.""" diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index 6efa3dcb80c..4fe05279919 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -10,10 +10,10 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import (DOMAIN, LockDevice, PLATFORM_SCHEMA) +from homeassistant.components.lock import DOMAIN, PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids REQUIREMENTS = ['pynuki==1.3.1'] @@ -25,7 +25,12 @@ DEFAULT_PORT = 8080 ATTR_BATTERY_CRITICAL = 'battery_critical' ATTR_NUKI_ID = 'nuki_id' ATTR_UNLATCH = 'unlatch' + +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + NUKI_DATA = 'nuki' + SERVICE_LOCK_N_GO = 'nuki_lock_n_go' SERVICE_UNLATCH = 'nuki_unlatch' @@ -44,9 +49,6 @@ UNLATCH_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -93,7 +95,7 @@ class NukiLock(LockDevice): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" if NUKI_DATA not in self.hass.data: self.hass.data[NUKI_DATA] = {} if DOMAIN not in self.hass.data[NUKI_DATA]: diff --git a/homeassistant/components/lock/tesla.py b/homeassistant/components/lock/tesla.py index 80a35adb5fb..4d24ed20003 100644 --- a/homeassistant/components/lock/tesla.py +++ b/homeassistant/components/lock/tesla.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/lock.tesla/ import logging from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice -from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN +from homeassistant.components.tesla import TeslaDevice from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED _LOGGER = logging.getLogger(__name__) @@ -26,7 +27,7 @@ class TeslaLock(TeslaDevice, LockDevice): """Representation of a Tesla door lock.""" def __init__(self, tesla_device, controller): - """Initialisation of the lock.""" + """Initialise of the lock.""" self._state = None super().__init__(tesla_device, controller) self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) @@ -47,7 +48,7 @@ class TeslaLock(TeslaDevice, LockDevice): return self._state == STATE_LOCKED def update(self): - """Updating state of the lock.""" + """Update state of the lock.""" _LOGGER.debug("Updating state for: %s", self._name) self.tesla_device.update() self._state = STATE_LOCKED if self.tesla_device.is_locked() \ diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 118a8d8f664..a5cd18454df 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -10,9 +10,9 @@ import logging import voluptuous as vol from homeassistant.components.lock import LockDevice -from homeassistant.components.wink import WinkDevice, DOMAIN +from homeassistant.components.wink import DOMAIN, WinkDevice +from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN import homeassistant.helpers.config_validation as cv -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, ATTR_CODE DEPENDENCIES = ['wink'] @@ -30,13 +30,19 @@ ATTR_SENSITIVITY = 'sensitivity' ATTR_MODE = 'mode' ATTR_NAME = 'name' -ALARM_SENSITIVITY_MAP = {"low": 0.2, "medium_low": 0.4, - "medium": 0.6, "medium_high": 0.8, - "high": 1.0} +ALARM_SENSITIVITY_MAP = { + 'low': 0.2, + 'medium_low': 0.4, + 'medium': 0.6, + 'medium_high': 0.8, + 'high': 1.0, +} -ALARM_MODES_MAP = {"tamper": "tamper", - "activity": "alert", - "forced_entry": "forced_entry"} +ALARM_MODES_MAP = { + 'activity': 'alert', + 'forced_entry': 'forced_entry', + 'tamper': 'tamper', +} SET_ENABLED_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -70,7 +76,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkLockDevice(lock, hass)]) def service_handle(service): - """Handler for services.""" + """Handle for services.""" entity_ids = service.data.get('entity_id') all_locks = hass.data[DOMAIN]['entities']['lock'] locks_to_set = [] @@ -127,7 +133,7 @@ class WinkLockDevice(WinkDevice, LockDevice): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['lock'].append(self) @property diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index 819844325d1..bef821220b3 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -37,7 +37,7 @@ def setup(hass, base_config): from pylutron import Lutron hass.data[LUTRON_CONTROLLER] = None - hass.data[LUTRON_DEVICES] = {'light': []} + hass.data[LUTRON_DEVICES] = {'light': [], 'cover': []} config = base_config.get(DOMAIN) hass.data[LUTRON_CONTROLLER] = Lutron( @@ -50,9 +50,12 @@ def setup(hass, base_config): # Sort our devices into types for area in hass.data[LUTRON_CONTROLLER].areas: for output in area.outputs: - hass.data[LUTRON_DEVICES]['light'].append((area.name, output)) + if output.type == 'SYSTEM_SHADE': + hass.data[LUTRON_DEVICES]['cover'].append((area.name, output)) + else: + hass.data[LUTRON_DEVICES]['light'].append((area.name, output)) - for component in ('light',): + for component in ('light', 'cover'): discovery.load_platform(hass, component, DOMAIN, None, base_config) return True diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 4f999649a44..a1e68555649 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -82,7 +82,7 @@ def async_setup(hass, config): mailbox_entity = MailboxEntity(hass, mailbox) component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_add_entity(mailbox_entity) + yield from component.async_add_entities([mailbox_entity]) setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index 2b204e584c3..b8293f64fc0 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -1,17 +1,17 @@ -""" -Provides a map panel for showing device locations. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/map/ -""" -import asyncio - -DOMAIN = 'map' - - -@asyncio.coroutine -def async_setup(hass, config): - """Register the built-in map panel.""" - yield from hass.components.frontend.async_register_built_in_panel( - 'map', 'map', 'mdi:account-location') - return True +""" +Provides a map panel for showing device locations. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/map/ +""" +import asyncio + +DOMAIN = 'map' + + +@asyncio.coroutine +def async_setup(hass, config): + """Register the built-in map panel.""" + yield from hass.components.frontend.async_register_built_in_panel( + 'map', 'map', 'mdi:account-location') + return True diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 645a418cf8c..de56c5140e9 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2017.12.28'] +REQUIREMENTS = ['youtube_dl==2018.01.14'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 44e6810fd5d..91bcb4d8af0 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -366,7 +366,7 @@ def async_setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.http.register_view(MediaPlayerImageView(component.entities)) + hass.http.register_view(MediaPlayerImageView(component)) yield from component.async_setup(config) @@ -929,14 +929,14 @@ class MediaPlayerImageView(HomeAssistantView): url = '/api/media_player_proxy/{entity_id}' name = 'api:media_player:image' - def __init__(self, entities): + def __init__(self, component): """Initialize a media player view.""" - self.entities = entities + self.component = component @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" - player = self.entities.get(entity_id) + player = self.component.get_entity(entity_id) if player is None: status = 404 if request[KEY_AUTHENTICATED] else 401 return web.Response(status=status) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 1f86056efb5..ca6b152a37e 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -1,5 +1,5 @@ """ -Bluesound. +Support for Bluesound devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.bluesound/ @@ -16,14 +16,14 @@ import async_timeout import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PLAY, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PAUSE, PLATFORM_SCHEMA, - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PREVIOUS_TRACK, + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_HOSTS, STATE_IDLE, STATE_PAUSED, - STATE_PLAYING, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) + CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -60,6 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def _add_player(hass, async_add_devices, host, port=None, name=None): + """Add Bluesound players.""" if host in [x.host for x in hass.data[DATA_BLUESOUND]]: return @@ -108,8 +109,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass.data[DATA_BLUESOUND] = [] if discovery_info: - _add_player(hass, async_add_devices, discovery_info.get('host'), - discovery_info.get('port', None)) + _add_player(hass, async_add_devices, discovery_info.get(CONF_HOST), + discovery_info.get(CONF_PORT, None)) return hosts = config.get(CONF_HOSTS, None) @@ -117,11 +118,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for host in hosts: _add_player( hass, async_add_devices, host.get(CONF_HOST), - host.get(CONF_PORT), host.get(CONF_NAME, None)) + host.get(CONF_PORT), host.get(CONF_NAME)) class BluesoundPlayer(MediaPlayerDevice): - """Bluesound Player Object.""" + """Represenatation of a Bluesound Player.""" def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" @@ -150,6 +151,7 @@ class BluesoundPlayer(MediaPlayerDevice): @staticmethod def _try_get_index(string, seach_string): + """Get the index.""" try: return string.index(seach_string) except ValueError: @@ -158,6 +160,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine def _internal_update_sync_status( self, on_updated_cb=None, raise_timeout=False): + """Update the internal status.""" resp = None try: resp = yield from self.send_bluesound_command( @@ -186,7 +189,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine def _start_poll_command(self): - """"Loop which polls the status of the player.""" + """Loop which polls the status of the player.""" try: while True: yield from self.async_update_status() @@ -214,7 +217,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine def async_init(self): - """Initiate the player async.""" + """Initialize the player async.""" try: if self._retry_remove is not None: self._retry_remove() @@ -284,7 +287,7 @@ class BluesoundPlayer(MediaPlayerDevice): @asyncio.coroutine def async_update_status(self): - """Using the poll session to always get the status of the player.""" + """Use the poll session to always get the status of the player.""" import xmltodict response = None diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index c95ddcab97e..d26fce0ea88 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -8,14 +8,13 @@ import logging import voluptuous as vol -from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, - STATE_OFF, STATE_ON) -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - DOMAIN, MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, - SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) - + DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymonoprice==0.3'] @@ -42,9 +41,9 @@ SERVICE_SNAPSHOT = 'snapshot' SERVICE_RESTORE = 'restore' # Valid zone ids: 11-16 or 21-26 or 31-36 -ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16), - vol.Range(min=21, max=26), - vol.Range(min=31, max=36))) +ZONE_IDS = vol.All(vol.Coerce(int), vol.Any( + vol.Range(min=11, max=16), vol.Range(min=21, max=26), + vol.Range(min=31, max=36))) # Valid source ids: 1-6 SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6)) @@ -66,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: monoprice = get_monoprice(port) except SerialException: - _LOGGER.error('Error connecting to Monoprice controller.') + _LOGGER.error("Error connecting to Monoprice controller") return sources = {source_id: extra[CONF_NAME] for source_id, extra @@ -75,9 +74,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.data[DATA_MONOPRICE] = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - hass.data[DATA_MONOPRICE].append(MonopriceZone(monoprice, sources, - zone_id, - extra[CONF_NAME])) + hass.data[DATA_MONOPRICE].append(MonopriceZone( + monoprice, sources, zone_id, extra[CONF_NAME])) add_devices(hass.data[DATA_MONOPRICE], True) @@ -98,19 +96,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device.restore() hass.services.register( - DOMAIN, SERVICE_SNAPSHOT, service_handle, - schema=MEDIA_PLAYER_SCHEMA) + DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=MEDIA_PLAYER_SCHEMA) hass.services.register( - DOMAIN, SERVICE_RESTORE, service_handle, - schema=MEDIA_PLAYER_SCHEMA) + DOMAIN, SERVICE_RESTORE, service_handle, schema=MEDIA_PLAYER_SCHEMA) class MonopriceZone(MediaPlayerDevice): """Representation of a a Monoprice amplifier zone.""" - # pylint: disable=too-many-public-methods - def __init__(self, monoprice, sources, zone_id, zone_name): """Initialize new zone.""" self._monoprice = monoprice @@ -179,7 +173,7 @@ class MonopriceZone(MediaPlayerDevice): @property def source(self): - """"Return the current input source of the device.""" + """Return the current input source of the device.""" return self._source @property @@ -224,12 +218,10 @@ class MonopriceZone(MediaPlayerDevice): """Volume up the media player.""" if self._volume is None: return - self._monoprice.set_volume(self._zone_id, - min(self._volume + 1, 38)) + self._monoprice.set_volume(self._zone_id, min(self._volume + 1, 38)) def volume_down(self): """Volume down media player.""" if self._volume is None: return - self._monoprice.set_volume(self._zone_id, - max(self._volume - 1, 0)) + self._monoprice.set_volume(self._zone_id, max(self._volume - 1, 0)) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index c661e2a3b58..4307b68e709 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -4,22 +4,22 @@ Support to interact with a Music Player Daemon. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.mpd/ """ +from datetime import timedelta import logging import os -from datetime import timedelta import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_PLAYLIST, - SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET, - SUPPORT_SEEK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_PAUSED, STATE_PLAYING, - CONF_PORT, CONF_PASSWORD, CONF_HOST, CONF_NAME) + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -116,7 +116,7 @@ class MpdDevice(MediaPlayerDevice): @property def available(self): - """True if MPD is available and connected.""" + """Return true if MPD is available and connected.""" return self._is_connected def update(self): diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index 43355782d29..2f0c49b2583 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['nad_receiver==0.0.6'] +REQUIREMENTS = ['nad_receiver==0.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/nadtcp.py b/homeassistant/components/media_player/nadtcp.py index a59a032f624..06ec3c04cbe 100644 --- a/homeassistant/components/media_player/nadtcp.py +++ b/homeassistant/components/media_player/nadtcp.py @@ -5,17 +5,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.nadtcp/ """ import logging + import voluptuous as vol + from homeassistant.components.media_player import ( - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, - PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_NAME, STATE_OFF, STATE_ON) + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['nad_receiver==0.0.6'] +REQUIREMENTS = ['nad_receiver==0.0.9'] _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,6 @@ SUPPORT_NAD = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | \ CONF_MIN_VOLUME = 'min_volume' CONF_MAX_VOLUME = 'max_volume' CONF_VOLUME_STEP = 'volume_step' -CONF_HOST = 'host' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -42,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the NAD platform.""" + """Set up the NAD platform.""" from nad_receiver import NADReceiverTCP add_devices([NADtcp( NADReceiverTCP(config.get(CONF_HOST)), diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 5917f1e3083..15b16eec11b 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-roku==3.1.3'] +REQUIREMENTS = ['python-roku==3.1.5'] KNOWN_HOSTS = [] DEFAULT_PORT = 8060 diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index d42bd9ea012..57f25873ae7 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -121,9 +121,14 @@ class SamsungTVDevice(MediaPlayerDevice): self._config['method'] = 'legacy' def update(self): - """Retrieve the latest data.""" - # Send an empty key to see if we are still connected - self.send_key('KEY') + """Update state of device.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(self._config[CONF_TIMEOUT]) + sock.connect((self._config['host'], self._config['port'])) + self._state = STATE_ON + except socket.error: + self._state = STATE_OFF def get_remote(self): """Create or return a remote control instance.""" diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index fe8280fb2ab..3e5ee57cb2f 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -320,3 +320,17 @@ squeezebox_call_method: parameters: description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. example: '["loadtracks", "track.titlesearch=highway to hell"]' + +yamaha_enable_output: + description: Enable or disable an output port + + fields: + entity_id: + description: Name(s) of entites to enable/disable port on. + example: 'media_player.yamaha' + port: + description: Name of port to enable/disable. + example: 'hdmi1' + enabled: + description: Boolean indicating if port should be enabled or not. + example: true diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 2413de136ab..793800a3d22 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -11,11 +11,11 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, - DOMAIN, PLATFORM_SCHEMA, MediaPlayerDevice) + DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, - CONF_PORT, ATTR_ENTITY_ID) + ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['snapcast==2.0.8'] @@ -42,14 +42,14 @@ SERVICE_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port + vol.Optional(CONF_PORT): cv.port, }) # pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup the Snapcast platform.""" + """Set up the Snapcast platform.""" import snapcast.control from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) @@ -68,25 +68,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from device.async_restore() hass.services.async_register( - DOMAIN, SERVICE_SNAPSHOT, _handle_service, - schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_SNAPSHOT, _handle_service, schema=SERVICE_SCHEMA) hass.services.async_register( - DOMAIN, SERVICE_RESTORE, _handle_service, - schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_RESTORE, _handle_service, schema=SERVICE_SCHEMA) try: server = yield from snapcast.control.create_server( hass.loop, host, port, reconnect=True) except socket.gaierror: - _LOGGER.error('Could not connect to Snapcast server at %s:%d', + _LOGGER.error("Could not connect to Snapcast server at %s:%d", host, port) - return False + return + groups = [SnapcastGroupDevice(group) for group in server.groups] clients = [SnapcastClientDevice(client) for client in server.clients] devices = groups + clients hass.data[DATA_KEY] = devices async_add_devices(devices) - return True class SnapcastGroupDevice(MediaPlayerDevice): diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 0c6d380e81e..d4a7fd3adb5 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -14,14 +14,14 @@ import urllib import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP, - SUPPORT_PLAY, SUPPORT_SHUFFLE_SET) + ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, - CONF_HOSTS, ATTR_TIME) + ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -126,7 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info: player = soco.SoCo(discovery_info.get('host')) - # if device already exists by config + # If device already exists by config if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]: return @@ -167,7 +167,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(coordinators, True) if slaves: add_devices(slaves, True) - _LOGGER.info("Added %s Sonos speakers", len(players)) + _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" @@ -242,10 +242,11 @@ def _parse_timespan(timespan): reversed(timespan.split(':')))) -class _ProcessSonosEventQueue(): +class _ProcessSonosEventQueue(object): """Queue like object for dispatching sonos events.""" def __init__(self, sonos_device): + """Initialize Sonos event queue.""" self._sonos_device = sonos_device def put(self, item, block=True, timeout=None): @@ -263,27 +264,14 @@ def _get_entity_from_soco(hass, soco): raise ValueError("No entity for SoCo device") -def soco_error(funct): - """Catch soco exceptions.""" - @ft.wraps(funct) - def wrapper(*args, **kwargs): - """Wrap for all soco exception.""" - from soco.exceptions import SoCoException - try: - return funct(*args, **kwargs) - except SoCoException as err: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - return wrapper - - -def soco_filter_upnperror(errorcodes=None): - """Filter out specified UPnP errors from logs.""" +def soco_error(errorcodes=None): + """Filter out specified UPnP errors from logs and avoid exceptions.""" def decorator(funct): - """Decorator function.""" + """Decorate functions.""" @ft.wraps(funct) def wrapper(*args, **kwargs): """Wrap for all soco UPnP exception.""" - from soco.exceptions import SoCoUPnPException + from soco.exceptions import SoCoUPnPException, SoCoException # Temporarily disable SoCo logging because it will log the # UPnP exception otherwise @@ -295,7 +283,9 @@ def soco_filter_upnperror(errorcodes=None): if err.error_code in errorcodes: pass else: - raise + _LOGGER.error("Error on %s with %s", funct.__name__, err) + except SoCoException as err: + _LOGGER.error("Error on %s with %s", funct.__name__, err) finally: _SOCO_SERVICES_LOGGER.disabled = False @@ -901,32 +891,32 @@ class SonosDevice(MediaPlayerDevice): return supported - @soco_error + @soco_error() def volume_up(self): """Volume up media player.""" self._player.volume += self.volume_increment - @soco_error + @soco_error() def volume_down(self): """Volume down media player.""" self._player.volume -= self.volume_increment - @soco_error + @soco_error() def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._player.volume = str(int(volume * 100)) - @soco_error + @soco_error() def set_shuffle(self, shuffle): """Enable/Disable shuffle mode.""" self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL' - @soco_error + @soco_error() def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" self._player.mute = mute - @soco_error + @soco_error() @soco_coordinator def select_source(self, source): """Select input source.""" @@ -1008,64 +998,61 @@ class SonosDevice(MediaPlayerDevice): return self._source_name - @soco_error + @soco_error() def turn_off(self): """Turn off media player.""" if self._support_stop: self.media_stop() - @soco_error - @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE) + @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def media_play(self): """Send play command.""" self._player.play() - @soco_error - @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE) + @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def media_stop(self): """Send stop command.""" self._player.stop() - @soco_error - @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE) + @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def media_pause(self): """Send pause command.""" self._player.pause() - @soco_error + @soco_error() @soco_coordinator def media_next_track(self): """Send next track command.""" self._player.next() - @soco_error + @soco_error() @soco_coordinator def media_previous_track(self): """Send next track command.""" self._player.previous() - @soco_error + @soco_error() @soco_coordinator def media_seek(self, position): """Send seek command.""" self._player.seek(str(datetime.timedelta(seconds=int(position)))) - @soco_error + @soco_error() @soco_coordinator def clear_playlist(self): """Clear players playlist.""" self._player.clear_queue() - @soco_error + @soco_error() def turn_on(self): """Turn the media player on.""" if self.support_play: self.media_play() - @soco_error + @soco_error() @soco_coordinator def play_media(self, media_type, media_id, **kwargs): """ @@ -1084,7 +1071,7 @@ class SonosDevice(MediaPlayerDevice): else: self._player.play_uri(media_id) - @soco_error + @soco_error() def join(self, master): """Join the player to a group.""" coord = [device for device in self.hass.data[DATA_SONOS] @@ -1099,13 +1086,13 @@ class SonosDevice(MediaPlayerDevice): else: _LOGGER.error("Master not found %s", master) - @soco_error + @soco_error() def unjoin(self): """Unjoin the player from a group.""" self._player.unjoin() self._coordinator = None - @soco_error + @soco_error() def snapshot(self, with_group=True): """Snapshot the player.""" from soco.snapshot import Snapshot @@ -1120,7 +1107,7 @@ class SonosDevice(MediaPlayerDevice): else: self._snapshot_group = None - @soco_error + @soco_error() def restore(self, with_group=True): """Restore snapshot for the player.""" from soco.exceptions import SoCoException @@ -1170,19 +1157,19 @@ class SonosDevice(MediaPlayerDevice): if s_dev != old.coordinator: s_dev.join(old.coordinator) - @soco_error + @soco_error() @soco_coordinator def set_sleep_timer(self, sleep_time): """Set the timer on the player.""" self._player.set_sleep_timer(sleep_time) - @soco_error + @soco_error() @soco_coordinator def clear_sleep_timer(self): """Clear the timer on the player.""" self._player.set_sleep_timer(None) - @soco_error + @soco_error() @soco_coordinator def update_alarm(self, **data): """Set the alarm clock on the player.""" @@ -1206,7 +1193,7 @@ class SonosDevice(MediaPlayerDevice): a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] a.save() - @soco_error + @soco_error() def update_option(self, **data): """Modify playback options.""" if ATTR_NIGHT_SOUND in data and self.night_sound is not None: diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index a7173e35a48..27a0714527d 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -154,7 +154,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): if allow_override and service_name in self._cmds: yield from async_call_from_config( self.hass, self._cmds[service_name], - variables=service_data, blocking=True) + variables=service_data, blocking=True, + validate_config=False) return active_child = self._child_state diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 5d6e6fcf6dd..64d1f642e6e 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -1,36 +1,23 @@ """ Vizio SmartCast TV support. -Usually only 2016+ models come with SmartCast capabilities. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.vizio/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.util as util from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, - SUPPORT_SELECT_SOURCE, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_NEXT_TRACK, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, - MediaPlayerDevice -) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - STATE_UNKNOWN, - STATE_OFF, - STATE_ON, - CONF_NAME, - CONF_HOST, - CONF_ACCESS_TOKEN -) + CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, + STATE_UNKNOWN) from homeassistant.helpers import config_validation as cv +import homeassistant.util as util REQUIREMENTS = ['pyvizio==0.0.2'] @@ -39,13 +26,16 @@ _LOGGER = logging.getLogger(__name__) CONF_SUPPRESS_WARNING = 'suppress_warning' CONF_VOLUME_STEP = 'volume_step' -ICON = 'mdi:television' DEFAULT_NAME = 'Vizio SmartCast' DEFAULT_VOLUME_STEP = 1 -DEVICE_NAME = 'Python Vizio' DEVICE_ID = 'pyvizio' -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +DEVICE_NAME = 'Python Vizio' + +ICON = 'mdi:television' + MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + SUPPORTED_COMMANDS = SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ | SUPPORT_SELECT_SOURCE \ | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ @@ -70,14 +60,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device = VizioDevice(host, token, name, volume_step) if device.validate_setup() is False: - _LOGGER.error('Failed to setup Vizio TV platform, ' - 'please check if host and API key are correct.') - return False + _LOGGER.error("Failed to setup Vizio TV platform, " + "please check if host and API key are correct") + return if config.get(CONF_SUPPRESS_WARNING): from requests.packages import urllib3 - _LOGGER.warning('InsecureRequestWarning is disabled ' - 'because of Vizio platform configuration.') + _LOGGER.warning("InsecureRequestWarning is disabled " + "because of Vizio platform configuration") urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) add_devices([device], True) @@ -184,5 +174,5 @@ class VizioDevice(MediaPlayerDevice): self._device.vol_down(num=self._volume_step) def validate_setup(self): - """Validating if host is available and key is correct.""" + """Validate if host is available and key is correct.""" return self._device.get_current_volume() is not None diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 9d3e0b90fa4..55179ed60a9 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -4,30 +4,26 @@ Support for interface with an LG webOS Smart TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.webostv/ """ -import logging import asyncio from datetime import timedelta +import logging from urllib.parse import urlparse import voluptuous as vol -import homeassistant.util as util from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, - MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_CUSTOMIZE, CONF_TIMEOUT, STATE_OFF, - STATE_PLAYING, STATE_PAUSED, - STATE_UNKNOWN, CONF_NAME, CONF_FILENAME) + CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT, + STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script +import homeassistant.util as util -REQUIREMENTS = ['pylgtv==0.1.7', - 'websockets==3.2', - 'wakeonlan==0.2.2'] +REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2', 'wakeonlan==0.2.2'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) @@ -48,17 +44,16 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) CUSTOMIZE_SCHEMA = vol.Schema({ - vol.Optional(CONF_SOURCES): - vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SOURCES): vol.All(cv.ensure_list, [cv.string]), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, }) @@ -142,7 +137,7 @@ def request_configuration( # pylint: disable=unused-argument def lgtv_configuration_callback(data): - """The actions to do when our configuration callback is called.""" + """Handle actions when configuration callback is called.""" setup_tv(host, name, customize, config, timeout, hass, add_devices, turn_on_action) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 10f7adccae0..577988bc58c 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -12,10 +12,10 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - MEDIA_TYPE_MUSIC, + MEDIA_TYPE_MUSIC, MEDIA_PLAYER_SCHEMA, DOMAIN, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, - STATE_PLAYING, STATE_IDLE) + STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['rxv==0.5.1'] @@ -31,7 +31,7 @@ CONF_ZONE_NAMES = 'zone_names' CONF_ZONE_IGNORE = 'zone_ignore' DEFAULT_NAME = 'Yamaha Receiver' -KNOWN = 'yamaha_known_receivers' +DATA_YAMAHA = 'yamaha_known_receivers' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -44,15 +44,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string}, }) +SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output' + +ATTR_PORT = 'port' +ATTR_ENABLED = 'enabled' + +ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_PORT): cv.string, + vol.Required(ATTR_ENABLED): cv.boolean +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Yamaha platform.""" import rxv - # keep track of configured receivers so that we don't end up + # Keep track of configured receivers so that we don't end up # discovering a receiver dynamically that we have static config - # for. - if hass.data.get(KNOWN, None) is None: - hass.data[KNOWN] = set() + # for. Map each device from its unique_id to an instance since + # YamahaDevice is not hashable (thus not possible to add to a set). + if hass.data.get(DATA_YAMAHA) is None: + hass.data[DATA_YAMAHA] = {} name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -66,9 +77,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): model = discovery_info.get('model_name') ctrl_url = discovery_info.get('control_url') desc_url = discovery_info.get('description_url') - if ctrl_url in hass.data[KNOWN]: - _LOGGER.info("%s already manually configured", ctrl_url) - return receivers = rxv.RXV( ctrl_url, model_name=model, friendly_name=name, unit_desc_url=desc_url).zone_controllers() @@ -83,13 +91,40 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host) receivers = rxv.RXV(ctrl_url, name).zone_controllers() + devices = [] for receiver in receivers: - if receiver.zone not in zone_ignore: - hass.data[KNOWN].add(receiver.ctrl_url) - add_devices([ - YamahaDevice(name, receiver, source_ignore, - source_names, zone_names) - ], True) + if receiver.zone in zone_ignore: + continue + + device = YamahaDevice(name, receiver, source_ignore, + source_names, zone_names) + + # Only add device if it's not already added + if device.unique_id not in hass.data[DATA_YAMAHA]: + hass.data[DATA_YAMAHA][device.unique_id] = device + devices.append(device) + else: + _LOGGER.debug('Ignoring duplicate receiver %s', name) + + def service_handler(service): + """Handle for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + devices = [device for device in hass.data[DATA_YAMAHA].values() + if not entity_ids or device.entity_id in entity_ids] + + for device in devices: + port = service.data[ATTR_PORT] + enabled = service.data[ATTR_ENABLED] + + device.enable_output(port, enabled) + device.schedule_update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_ENABLE_OUTPUT, service_handler, + schema=ENABLE_OUTPUT_SCHEMA) + + add_devices(devices) class YamahaDevice(MediaPlayerDevice): @@ -98,7 +133,7 @@ class YamahaDevice(MediaPlayerDevice): def __init__(self, name, receiver, source_ignore, source_names, zone_names): """Initialize the Yamaha Receiver.""" - self._receiver = receiver + self.receiver = receiver self._muted = False self._volume = 0 self._pwstate = STATE_OFF @@ -114,10 +149,15 @@ class YamahaDevice(MediaPlayerDevice): self._name = name self._zone = receiver.zone + @property + def unique_id(self): + """Return an unique ID.""" + return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) + def update(self): """Get the latest details from the device.""" - self._play_status = self._receiver.play_status() - if self._receiver.on: + self._play_status = self.receiver.play_status() + if self.receiver.on: if self._play_status is None: self._pwstate = STATE_ON elif self._play_status.playing: @@ -127,17 +167,17 @@ class YamahaDevice(MediaPlayerDevice): else: self._pwstate = STATE_OFF - self._muted = self._receiver.mute - self._volume = (self._receiver.volume / 100) + 1 + self._muted = self.receiver.mute + self._volume = (self.receiver.volume / 100) + 1 if self.source_list is None: self.build_source_list() - current_source = self._receiver.input + current_source = self.receiver.input self._current_source = self._source_names.get( current_source, current_source) - self._playback_support = self._receiver.get_playback_support() - self._is_playback_supported = self._receiver.is_playback_supported( + self._playback_support = self.receiver.get_playback_support() + self._is_playback_supported = self.receiver.is_playback_supported( self._current_source) def build_source_list(self): @@ -147,7 +187,7 @@ class YamahaDevice(MediaPlayerDevice): self._source_list = sorted( self._source_names.get(source, source) for source in - self._receiver.inputs() + self.receiver.inputs() if source not in self._source_ignore) @property @@ -203,42 +243,42 @@ class YamahaDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" - self._receiver.on = False + self.receiver.on = False def set_volume_level(self, volume): """Set volume level, range 0..1.""" receiver_vol = 100 - (volume * 100) negative_receiver_vol = -receiver_vol - self._receiver.volume = negative_receiver_vol + self.receiver.volume = negative_receiver_vol def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - self._receiver.mute = mute + self.receiver.mute = mute def turn_on(self): """Turn the media player on.""" - self._receiver.on = True - self._volume = (self._receiver.volume / 100) + 1 + self.receiver.on = True + self._volume = (self.receiver.volume / 100) + 1 def media_play(self): """Send play command.""" - self._call_playback_function(self._receiver.play, "play") + self._call_playback_function(self.receiver.play, "play") def media_pause(self): """Send pause command.""" - self._call_playback_function(self._receiver.pause, "pause") + self._call_playback_function(self.receiver.pause, "pause") def media_stop(self): """Send stop command.""" - self._call_playback_function(self._receiver.stop, "stop") + self._call_playback_function(self.receiver.stop, "stop") def media_previous_track(self): """Send previous track command.""" - self._call_playback_function(self._receiver.previous, "previous track") + self._call_playback_function(self.receiver.previous, "previous track") def media_next_track(self): """Send next track command.""" - self._call_playback_function(self._receiver.next, "next track") + self._call_playback_function(self.receiver.next, "next track") def _call_playback_function(self, function, function_text): import rxv @@ -250,7 +290,7 @@ class YamahaDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - self._receiver.input = self._reverse_mapping.get(source, source) + self.receiver.input = self._reverse_mapping.get(source, source) def play_media(self, media_type, media_id, **kwargs): """Play media from an ID. @@ -275,7 +315,11 @@ class YamahaDevice(MediaPlayerDevice): """ if media_type == "NET RADIO": - self._receiver.net_radio(media_id) + self.receiver.net_radio(media_id) + + def enable_output(self, port, enabled): + """Enable or disable an output port..""" + self.receiver.enable_output(port, enabled) @property def media_artist(self): diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 829c1124363..e61ed05ce10 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -161,7 +161,7 @@ def async_setup(hass, config): face.store.pop(g_id) entity = entities.pop(g_id) - yield from entity.async_remove() + hass.states.async_remove(entity.entity_id) except HomeAssistantError as err: _LOGGER.error("Can't delete group '%s' with error: %s", g_id, err) diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py new file mode 100644 index 00000000000..678cdf10c56 --- /dev/null +++ b/homeassistant/components/mychevy.py @@ -0,0 +1,130 @@ +""" +MyChevy Component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/mychevy/ +""" +from datetime import timedelta +import logging +import threading +import time + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +REQUIREMENTS = ["mychevy==0.1.1"] + +DOMAIN = 'mychevy' +UPDATE_TOPIC = DOMAIN +ERROR_TOPIC = DOMAIN + "_error" + +MYCHEVY_SUCCESS = "success" +MYCHEVY_ERROR = "error" + +NOTIFICATION_ID = 'mychevy_website_notification' +NOTIFICATION_TITLE = 'MyChevy website status' + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) +ERROR_SLEEP_TIME = timedelta(minutes=30) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +class EVSensorConfig(object): + """The EV sensor configuration.""" + + def __init__(self, name, attr, unit_of_measurement=None, icon=None): + """Create new sensor configuration.""" + self.name = name + self.attr = attr + self.unit_of_measurement = unit_of_measurement + self.icon = icon + + +class EVBinarySensorConfig(object): + """The EV binary sensor configuration.""" + + def __init__(self, name, attr, device_class=None): + """Create new binary sensor configuration.""" + self.name = name + self.attr = attr + self.device_class = device_class + + +def setup(hass, base_config): + """Set up the mychevy component.""" + import mychevy.mychevy as mc + + config = base_config.get(DOMAIN) + + email = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass) + hass.data[DOMAIN].start() + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + return True + + +class MyChevyHub(threading.Thread): + """MyChevy Hub. + + Connecting to the mychevy website is done through a selenium + webscraping process. That can only run synchronously. In order to + prevent blocking of other parts of Home Assistant the architecture + launches a polling loop in a thread. + + When new data is received, sensors are updated, and hass is + signaled that there are updates. Sensors are not created until the + first update, which will be 60 - 120 seconds after the platform + starts. + """ + + def __init__(self, client, hass): + """Initialize MyChevy Hub.""" + super().__init__() + self._client = client + self.hass = hass + self.car = None + self.status = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update sensors from mychevy website. + + This is a synchronous polling call that takes a very long time + (like 2 to 3 minutes long time) + + """ + self.car = self._client.data() + + def run(self): + """Thread run loop.""" + # We add the status device first outside of the loop + + # And then busy wait on threads + while True: + try: + _LOGGER.info("Starting mychevy loop") + self.update() + self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error updating mychevy data. " + "This probably means the OnStar link is down again") + self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) + time.sleep(ERROR_SLEEP_TIME.seconds) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 71be416c59c..91053b41bf6 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -1,8 +1,8 @@ """ Connect to a MySensors gateway via pymysensors API. -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.mysensors/ +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mysensors/ """ import asyncio from collections import defaultdict @@ -115,21 +115,20 @@ def is_serial_port(value): if value in ports: return value else: - raise vol.Invalid( - '{} is not a serial port'.format(value)) + raise vol.Invalid('{} is not a serial port'.format(value)) else: return cv.isdevice(value) def deprecated(key): - """Mark key as deprecated in config.""" + """Mark key as deprecated in configuration.""" def validator(config): """Check if key is in config, log warning and remove key.""" if key not in config: return config _LOGGER.warning( '%s option for %s is deprecated. Please remove %s from your ' - 'configuration file.', key, DOMAIN, key) + 'configuration file', key, DOMAIN, key) config.pop(key) return config return validator @@ -150,16 +149,13 @@ CONFIG_SCHEMA = vol.Schema({ vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port), vol.Optional(CONF_PERSISTENCE_FILE): vol.All(cv.string, is_persistence_file, has_parent_dir), - vol.Optional( - CONF_BAUD_RATE, - default=DEFAULT_BAUD_RATE): cv.positive_int, - vol.Optional( - CONF_TCP_PORT, - default=DEFAULT_TCP_PORT): cv.port, - vol.Optional( - CONF_TOPIC_IN_PREFIX, default=''): valid_subscribe_topic, - vol.Optional( - CONF_TOPIC_OUT_PREFIX, default=''): valid_publish_topic, + vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): + cv.positive_int, + vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, + vol.Optional(CONF_TOPIC_IN_PREFIX, default=''): + valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX, default=''): + valid_publish_topic, vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, }] ), @@ -171,7 +167,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# mysensors const schemas +# MySensors const schemas BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} LIGHT_DIMMER_SCHEMA = { @@ -439,7 +435,7 @@ def validate_child(gateway, node_id, child): def discover_mysensors_platform(hass, platform, new_devices): - """Discover a mysensors platform.""" + """Discover a MySensors platform.""" discovery.load_platform( hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) @@ -458,7 +454,7 @@ def discover_persistent_devices(hass, gateway): def get_mysensors_devices(hass, domain): - """Return mysensors devices for a platform.""" + """Return MySensors devices for a platform.""" if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] @@ -467,15 +463,14 @@ def get_mysensors_devices(hass, domain): def gw_callback_factory(hass): """Return a new callback for the gateway.""" def mysensors_callback(msg): - """Default callback for a mysensors gateway.""" + """Handle messages from a MySensors gateway.""" start = timer() _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) if child is None: - _LOGGER.debug( - "Not a child update for node %s", msg.node_id) + _LOGGER.debug("Not a child update for node %s", msg.node_id) return signals = [] @@ -518,7 +513,7 @@ def get_mysensors_name(gateway, node_id, child_id): def get_mysensors_gateway(hass, gateway_id): - """Return gateway.""" + """Return MySensors gateway.""" if MYSENSORS_GATEWAYS not in hass.data: hass.data[MYSENSORS_GATEWAYS] = {} gateways = hass.data.get(MYSENSORS_GATEWAYS) @@ -528,8 +523,8 @@ def get_mysensors_gateway(hass, gateway_id): def setup_mysensors_platform( hass, domain, discovery_info, device_class, device_args=None, add_devices=None): - """Set up a mysensors platform.""" - # Only act if called via mysensors by discovery event. + """Set up a MySensors platform.""" + # Only act if called via MySensors by discovery event. # Otherwise gateway is not setup. if not discovery_info: return @@ -627,7 +622,7 @@ class MySensorsEntity(MySensorsDevice, Entity): @property def should_poll(self): - """Mysensor gateway pushes its state to HA.""" + """Return the polling state. The gateway pushes its states.""" return False @property diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index bd680b5361e..7402bb18843 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,8 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.4.zip' - '#pybotvac==0.0.4'] +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.5.zip' + '#pybotvac==0.0.5'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py index 543ce434a8d..2b2cb4e7f22 100644 --- a/homeassistant/components/notify/clicksend.py +++ b/homeassistant/components/notify/clicksend.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import ( - CONF_API_KEY, CONF_USERNAME, CONF_RECIPIENT, CONTENT_TYPE_JSON) + CONF_API_KEY, CONF_RECIPIENT, CONF_SENDER, CONF_USERNAME, + CONTENT_TYPE_JSON) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -23,15 +24,27 @@ BASE_API_URL = 'https://rest.clicksend.com/v3' HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, -}) + +def validate_sender(config): + """Set the optional sender name if sender name is not provided.""" + if CONF_SENDER in config: + return config + config[CONF_SENDER] = config[CONF_RECIPIENT] + return config + + +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.Optional(CONF_SENDER): cv.string, + }), validate_sender)) def get_service(hass, config, discovery_info=None): """Get the ClickSend notification service.""" + print("#### ", config) if _authenticate(config) is False: _LOGGER.exception("You are not authorized to access ClickSend") return None @@ -47,16 +60,26 @@ class ClicksendNotificationService(BaseNotificationService): self.username = config.get(CONF_USERNAME) self.api_key = config.get(CONF_API_KEY) self.recipient = 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.recipient, - 'to': self.recipient, 'body': message}]}) + data = ({ + 'messages': [ + { + 'source': 'hass.notify', + 'from': self.sender, + 'to': self.recipient, + 'body': message, + } + ] + }) api_url = "{}/sms/send".format(BASE_API_URL) - resp = requests.post(api_url, data=json.dumps(data), headers=HEADERS, - auth=(self.username, self.api_key), timeout=5) + resp = requests.post( + api_url, data=json.dumps(data), headers=HEADERS, + auth=(self.username, self.api_key), timeout=5) obj = json.loads(resp.text) response_msg = obj['response_msg'] @@ -70,9 +93,9 @@ class ClicksendNotificationService(BaseNotificationService): def _authenticate(config): """Authenticate with ClickSend.""" api_url = '{}/account'.format(BASE_API_URL) - resp = requests.get(api_url, headers=HEADERS, - auth=(config.get(CONF_USERNAME), - config.get(CONF_API_KEY)), timeout=5) + resp = requests.get( + api_url, headers=HEADERS, auth=(config.get(CONF_USERNAME), + config.get(CONF_API_KEY)), timeout=5) if resp.status_code != 200: return False diff --git a/homeassistant/components/notify/prowl.py b/homeassistant/components/notify/prowl.py index 1298657a69a..3928fa81167 100644 --- a/homeassistant/components/notify/prowl.py +++ b/homeassistant/components/notify/prowl.py @@ -1,70 +1,70 @@ -""" -Prowl notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.prowl/ -""" -import logging -import asyncio - -import async_timeout -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA, - BaseNotificationService) -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://api.prowlapp.com/publicapi/' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, -}) - - -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): - """Get the Prowl notification service.""" - return ProwlNotificationService(hass, config[CONF_API_KEY]) - - -class ProwlNotificationService(BaseNotificationService): - """Implement the notification service for Prowl.""" - - def __init__(self, hass, api_key): - """Initialize the service.""" - self._hass = hass - self._api_key = api_key - - @asyncio.coroutine - def async_send_message(self, message, **kwargs): - """Send the message to the user.""" - response = None - session = None - url = '{}{}'.format(_RESOURCE, 'add') - data = kwargs.get(ATTR_DATA) - payload = { - 'apikey': self._api_key, - 'application': 'Home-Assistant', - 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - 'description': message, - 'priority': data['priority'] if data and 'priority' in data else 0 - } - - _LOGGER.debug("Attempting call Prowl service at %s", url) - session = async_get_clientsession(self._hass) - - try: - with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from session.post(url, data=payload) - result = yield from response.text() - - if response.status != 200 or 'error' in result: - _LOGGER.error("Prowl service returned http " - "status %d, response %s", - response.status, result) - except asyncio.TimeoutError: - _LOGGER.error("Timeout accessing Prowl at %s", url) +""" +Prowl notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.prowl/ +""" +import logging +import asyncio + +import async_timeout +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://api.prowlapp.com/publicapi/' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, +}) + + +@asyncio.coroutine +def async_get_service(hass, config, discovery_info=None): + """Get the Prowl notification service.""" + return ProwlNotificationService(hass, config[CONF_API_KEY]) + + +class ProwlNotificationService(BaseNotificationService): + """Implement the notification service for Prowl.""" + + def __init__(self, hass, api_key): + """Initialize the service.""" + self._hass = hass + self._api_key = api_key + + @asyncio.coroutine + def async_send_message(self, message, **kwargs): + """Send the message to the user.""" + response = None + session = None + url = '{}{}'.format(_RESOURCE, 'add') + data = kwargs.get(ATTR_DATA) + payload = { + 'apikey': self._api_key, + 'application': 'Home-Assistant', + 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + 'description': message, + 'priority': data['priority'] if data and 'priority' in data else 0 + } + + _LOGGER.debug("Attempting call Prowl service at %s", url) + session = async_get_clientsession(self._hass) + + try: + with async_timeout.timeout(10, loop=self._hass.loop): + response = yield from session.post(url, data=payload) + result = yield from response.text() + + if response.status != 200 or 'error' in result: + _LOGGER.error("Prowl service returned http " + "status %d, response %s", + response.status, result) + except asyncio.TimeoutError: + _LOGGER.error("Timeout accessing Prowl at %s", url) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 359810bb6bc..37edb6709a7 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -129,7 +129,7 @@ class PushBulletNotificationService(BaseNotificationService): continue def _push_data(self, message, title, data, pusher, email=None): - """Helper for creating the message content.""" + """Create the message content.""" from pushbullet import PushError if data is None: data = {} @@ -138,8 +138,11 @@ class PushBulletNotificationService(BaseNotificationService): filepath = data.get(ATTR_FILE) file_url = data.get(ATTR_FILE_URL) try: + email_kwargs = {} + if email: + email_kwargs['email'] = email if url: - pusher.push_link(title, url, body=message, email=email) + pusher.push_link(title, url, body=message, **email_kwargs) elif filepath: if not self.hass.config.is_allowed_path(filepath): _LOGGER.error("Filepath is not valid or allowed") @@ -149,20 +152,21 @@ class PushBulletNotificationService(BaseNotificationService): if filedata.get('file_type') == 'application/x-empty': _LOGGER.error("Can not send an empty file") return - + filedata.update(email_kwargs) pusher.push_file(title=title, body=message, - email=email, **filedata) + **filedata) elif file_url: if not file_url.startswith('http'): _LOGGER.error("URL should start with http or https") return - pusher.push_file(title=title, body=message, email=email, + pusher.push_file(title=title, body=message, file_name=file_url, file_url=file_url, file_type=(mimetypes - .guess_type(file_url)[0])) + .guess_type(file_url)[0]), + **email_kwargs) elif data_list: - pusher.push_note(title, data_list, email=email) + pusher.push_list(title, data_list, **email_kwargs) else: - pusher.push_note(title, message, email=email) + pusher.push_note(title, message, **email_kwargs) except PushError as err: _LOGGER.error("Notify failed: %s", err) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 523fa2d6859..7df990fa0e5 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -1,23 +1,24 @@ -""" -Component to monitor plants. +"""Component to monitor plants. For more details about this component, please refer to the documentation at https://home-assistant.io/components/plant/ """ import logging import asyncio - +from datetime import datetime, timedelta +from collections import deque import voluptuous as vol from homeassistant.const import ( STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE, - CONF_SENSORS, ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON) + CONF_SENSORS, ATTR_UNIT_OF_MEASUREMENT) from homeassistant.components import group import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change +from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -30,7 +31,13 @@ READING_CONDUCTIVITY = 'conductivity' READING_BRIGHTNESS = 'brightness' ATTR_PROBLEM = 'problem' +ATTR_SENSORS = 'sensors' PROBLEM_NONE = 'none' +ATTR_MAX_BRIGHTNESS_HISTORY = 'max_brightness' + +# we're not returning only one value, we're returning a dict here. So we need +# to have a separate literal for it to avoid confusion. +ATTR_DICT_OF_UNITS_OF_MEASUREMENT = 'unit_of_measurement_dict' CONF_MIN_BATTERY_LEVEL = 'min_' + READING_BATTERY CONF_MIN_TEMPERATURE = 'min_' + READING_TEMPERATURE @@ -41,6 +48,7 @@ CONF_MIN_CONDUCTIVITY = 'min_' + READING_CONDUCTIVITY CONF_MAX_CONDUCTIVITY = 'max_' + READING_CONDUCTIVITY CONF_MIN_BRIGHTNESS = 'min_' + READING_BRIGHTNESS CONF_MAX_BRIGHTNESS = 'max_' + READING_BRIGHTNESS +CONF_CHECK_DAYS = 'check_days' CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY CONF_SENSOR_MOISTURE = READING_MOISTURE @@ -67,6 +75,7 @@ PLANT_SCHEMA = vol.Schema({ vol.Optional(CONF_MAX_CONDUCTIVITY): cv.positive_int, vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int, vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int, + vol.Optional(CONF_CHECK_DAYS): cv.positive_int, }) DOMAIN = 'plant' @@ -82,6 +91,11 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +# Flag for enabling/disabling the loading of the history from the database. +# This feature is turned off right now as it's tests are not 100% stable. +ENABLE_LOAD_HISTORY = False + + @asyncio.coroutine def async_setup(hass, config): """Set up the Plant component.""" @@ -98,7 +112,6 @@ def async_setup(hass, config): entities.append(entity) yield from component.async_add_entities(entities) - return True @@ -113,31 +126,26 @@ class Plant(Entity): READING_BATTERY: { ATTR_UNIT_OF_MEASUREMENT: '%', 'min': CONF_MIN_BATTERY_LEVEL, - 'icon': 'mdi:battery-outline' }, READING_TEMPERATURE: { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, 'min': CONF_MIN_TEMPERATURE, 'max': CONF_MAX_TEMPERATURE, - 'icon': 'mdi:thermometer' }, READING_MOISTURE: { ATTR_UNIT_OF_MEASUREMENT: '%', 'min': CONF_MIN_MOISTURE, 'max': CONF_MAX_MOISTURE, - 'icon': 'mdi:water' }, READING_CONDUCTIVITY: { ATTR_UNIT_OF_MEASUREMENT: 'µS/cm', 'min': CONF_MIN_CONDUCTIVITY, 'max': CONF_MAX_CONDUCTIVITY, - 'icon': 'mdi:emoticon-poop' }, READING_BRIGHTNESS: { ATTR_UNIT_OF_MEASUREMENT: 'lux', 'min': CONF_MIN_BRIGHTNESS, 'max': CONF_MAX_BRIGHTNESS, - 'icon': 'mdi:white-balance-sunny' } } @@ -145,8 +153,11 @@ class Plant(Entity): """Initialize the Plant component.""" self._config = config self._sensormap = dict() + self._readingmap = dict() + self._unit_of_measurement = dict() for reading, entity_id in config['sensors'].items(): self._sensormap[entity_id] = reading + self._readingmap[reading] = entity_id self._state = STATE_UNKNOWN self._name = name self._battery = None @@ -154,9 +165,13 @@ class Plant(Entity): self._conductivity = None self._temperature = None self._brightness = None - self._icon = 'mdi:help-circle' self._problems = PROBLEM_NONE + self._conf_check_days = 3 # default check interval: 3 days + if CONF_CHECK_DAYS in self._config: + self._conf_check_days = self._config[CONF_CHECK_DAYS] + self._brightness_history = DailyHistory(self._conf_check_days) + @callback def state_changed(self, entity_id, _, new_state): """Update the sensor status. @@ -180,9 +195,14 @@ class Plant(Entity): self._conductivity = int(float(value)) elif reading == READING_BRIGHTNESS: self._brightness = int(float(value)) + self._brightness_history.add_measurement(self._brightness, + new_state.last_updated) else: raise _LOGGER.error("Unknown reading from sensor %s: %s", entity_id, value) + if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes: + self._unit_of_measurement[reading] = \ + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._update_state() def _update_state(self): @@ -192,28 +212,80 @@ class Plant(Entity): params = self.READINGS[sensor_name] value = getattr(self, '_{}'.format(sensor_name)) if value is not None: - if 'min' in params and params['min'] in self._config: - min_value = self._config[params['min']] - if value < min_value: - result.append('{} low'.format(sensor_name)) - self._icon = params['icon'] + if sensor_name == READING_BRIGHTNESS: + result.append(self._check_min( + sensor_name, self._brightness_history.max, params)) + else: + result.append(self._check_min(sensor_name, value, params)) + result.append(self._check_max(sensor_name, value, params)) - if 'max' in params and params['max'] in self._config: - max_value = self._config[params['max']] - if value > max_value: - result.append('{} high'.format(sensor_name)) - self._icon = params['icon'] + result = [r for r in result if r is not None] if result: self._state = STATE_PROBLEM - self._problems = ','.join(result) + self._problems = ', '.join(result) else: self._state = STATE_OK - self._icon = 'mdi:thumb-up' self._problems = PROBLEM_NONE _LOGGER.debug("New data processed") self.async_schedule_update_ha_state() + def _check_min(self, sensor_name, value, params): + """If configured, check the value against the defined minimum value.""" + if 'min' in params and params['min'] in self._config: + min_value = self._config[params['min']] + if value < min_value: + return '{} low'.format(sensor_name) + + def _check_max(self, sensor_name, value, params): + """If configured, check the value against the defined maximum value.""" + if 'max' in params and params['max'] in self._config: + max_value = self._config[params['max']] + if value > max_value: + return '{} high'.format(sensor_name) + return None + + @asyncio.coroutine + def async_added_to_hass(self): + """After being added to hass, load from history.""" + if ENABLE_LOAD_HISTORY and 'recorder' in self.hass.config.components: + # only use the database if it's configured + self.hass.async_add_job(self._load_history_from_db) + + @asyncio.coroutine + def _load_history_from_db(self): + """Load the history of the brightness values from the database. + + This only needs to be done once during startup. + """ + from homeassistant.components.recorder.models import States + start_date = datetime.now() - timedelta(days=self._conf_check_days) + entity_id = self._readingmap.get(READING_BRIGHTNESS) + if entity_id is None: + _LOGGER.debug("not reading the history from the database as " + "there is no brightness sensor configured.") + return + + _LOGGER.debug("initializing values for %s from the database", + self._name) + with session_scope(hass=self.hass) as session: + query = session.query(States).filter( + (States.entity_id == entity_id.lower()) and + (States.last_updated > start_date) + ).order_by(States.last_updated.asc()) + states = execute(query) + + for state in states: + # filter out all None, NaN and "unknown" states + # only keep real values + try: + self._brightness_history.add_measurement( + int(state.state), state.last_updated) + except ValueError: + pass + _LOGGER.debug("initializing from database completed") + self.async_schedule_update_ha_state() + @property def should_poll(self): """No polling needed.""" @@ -237,11 +309,59 @@ class Plant(Entity): sensor in the attributes of the device. """ attrib = { - ATTR_ICON: self._icon, ATTR_PROBLEM: self._problems, + ATTR_SENSORS: self._readingmap, + ATTR_DICT_OF_UNITS_OF_MEASUREMENT: self._unit_of_measurement, } for reading in self._sensormap.values(): attrib[reading] = getattr(self, '_{}'.format(reading)) + if self._brightness_history.max is not None: + attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max + return attrib + + +class DailyHistory(object): + """Stores one measurement per day for a maximum number of days. + + At the moment only the maximum value per day is kept. + """ + + def __init__(self, max_length): + """Create new DailyHistory with a maximum length of the history.""" + self.max_length = max_length + self._days = None + self._max_dict = dict() + self.max = None + + def add_measurement(self, value, timestamp=datetime.now()): + """Add a new measurement for a certain day.""" + day = timestamp.date() + if value is None: + return + if self._days is None: + self._days = deque() + self._add_day(day, value) + else: + current_day = self._days[-1] + if day == current_day: + self._max_dict[day] = max(value, self._max_dict[day]) + elif day > current_day: + self._add_day(day, value) + else: + _LOGGER.warning('received old measurement, not storing it!') + + self.max = max(self._max_dict.values()) + + def _add_day(self, day, value): + """Add a new day to the history. + + Deletes the oldest day, if the queue becomes too long. + """ + if len(self._days) == self.max_length: + oldest = self._days.popleft() + del self._max_dict[oldest] + self._days.append(day) + self._max_dict[day] = value diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index a56b40f3064..b49b280791a 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -12,11 +12,11 @@ import time import voluptuous as vol -import homeassistant.util.dt as dt_util from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename +import homeassistant.util.dt as dt_util REQUIREMENTS = ['restrictedpython==4.0b2'] @@ -185,7 +185,7 @@ class StubPrinter: class TimeWrapper: - """Wrapper of the time module.""" + """Wrap the time module.""" # Class variable, only going to warn once per Home Assistant run warned = False @@ -205,7 +205,7 @@ class TimeWrapper: attribute = getattr(time, attr) if callable(attribute): def wrapper(*args, **kw): - """Wrapper to return callable method if callable.""" + """Wrap to return callable method if callable.""" return attribute(*args, **kw) return wrapper else: diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 1668fce0f45..505c3a7b2b0 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -5,20 +5,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/raincloud/ """ import asyncio -import logging from datetime import timedelta +import logging +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.entity import Entity + ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) - -from requests.exceptions import HTTPError, ConnectTimeout +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval REQUIREMENTS = ['raincloudy==0.0.4'] @@ -115,7 +114,7 @@ def setup(hass, config): def hub_refresh(event_time): """Call Raincloud hub to refresh information.""" - _LOGGER.debug("Updating RainCloud Hub component.") + _LOGGER.debug("Updating RainCloud Hub component") hass.data[DATA_RAINCLOUD].data.update() dispatcher_send(hass, SIGNAL_UPDATE_RAINCLOUD) @@ -156,7 +155,7 @@ class RainCloudEntity(Entity): self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback) def _update_callback(self): - """Callback update method.""" + """Call update method.""" self.schedule_update_ha_state(True) @property @@ -175,5 +174,5 @@ class RainCloudEntity(Entity): @property def icon(self): - """Icon to use in the frontend, if any.""" + """Return the icon to use in the frontend, if any.""" return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py index e9d65b85c81..e3c1ab8ff88 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats.py @@ -9,11 +9,9 @@ import threading import time from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['raspihats==2.2.3', - 'smbus-cffi==0.5.1'] +REQUIREMENTS = ['raspihats==2.2.3', 'smbus-cffi==0.5.1'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,7 @@ I2C_HATS_MANAGER = 'I2CH_MNG' # pylint: disable=unused-argument def setup(hass, config): - """Setup the raspihats component.""" + """Set up the raspihats component.""" hass.data[I2C_HATS_MANAGER] = I2CHatsManager() def start_i2c_hats_keep_alive(event): @@ -73,13 +71,13 @@ class I2CHatsDIScanner(object): _CALLBACKS = "callbacks" def setup(self, i2c_hat): - """Setup I2C-HAT instance for digital inputs scanner.""" + """Set up the I2C-HAT instance for digital inputs scanner.""" if hasattr(i2c_hat, self._DIGITAL_INPUTS): digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) old_value = None - # add old value attribute + # Add old value attribute setattr(digital_inputs, self._OLD_VALUE, old_value) - # add callbacks dict attribute {channel: callback} + # Add callbacks dict attribute {channel: callback} setattr(digital_inputs, self._CALLBACKS, {}) def register_callback(self, i2c_hat, channel, callback): @@ -141,17 +139,15 @@ class I2CHatsManager(threading.Thread): self._i2c_hats[address] = i2c_hat status_word = i2c_hat.status # read status_word to reset bits _LOGGER.info( - log_message(self, i2c_hat, "registered", status_word) - ) + log_message(self, i2c_hat, "registered", status_word)) def run(self): """Keep alive for I2C-HATs.""" # pylint: disable=import-error from raspihats.i2c_hats import ResponseException - _LOGGER.info( - log_message(self, "starting") - ) + _LOGGER.info(log_message(self, "starting")) + while self._run: with self._lock: for i2c_hat in list(self._i2c_hats.values()): @@ -176,17 +172,13 @@ class I2CHatsManager(threading.Thread): ) setattr(i2c_hat, self._EXCEPTION, ex) time.sleep(0.05) - _LOGGER.info( - log_message(self, "exiting") - ) + _LOGGER.info(log_message(self, "exiting")) def _read_status(self, i2c_hat): """Read I2C-HATs status.""" status_word = i2c_hat.status if status_word.value != 0x00: - _LOGGER.error( - log_message(self, i2c_hat, status_word) - ) + _LOGGER.error(log_message(self, i2c_hat, status_word)) def start_keep_alive(self): """Start keep alive mechanism.""" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 51da2d470ea..1c9524223e5 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,33 +8,33 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/recorder/ """ import asyncio +from collections import namedtuple import concurrent.futures +from datetime import datetime, timedelta import logging import queue import threading import time -from collections import namedtuple -from datetime import datetime, timedelta -from typing import Optional, Dict + +from typing import Dict, Optional import voluptuous as vol -from homeassistant.core import ( - HomeAssistant, callback, CoreState) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_ENTITIES, CONF_EXCLUDE, CONF_DOMAINS, - CONF_INCLUDE, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, - EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) + ATTR_ENTITY_ID, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, + EVENT_TIME_CHANGED, MATCH_ALL) +from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -from . import purge, migration +from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.0'] +REQUIREMENTS = ['sqlalchemy==1.2.1'] _LOGGER = logging.getLogger(__name__) @@ -140,9 +140,9 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle calls to the purge service.""" instance.do_adhoc_purge(service.data[ATTR_KEEP_DAYS]) - hass.services.async_register(DOMAIN, SERVICE_PURGE, - async_handle_purge_service, - schema=SERVICE_PURGE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_PURGE, async_handle_purge_service, + schema=SERVICE_PURGE_SCHEMA) return (yield from instance.async_db_ready) @@ -169,10 +169,9 @@ class Recorder(threading.Thread): self.engine = None # type: Any self.run_info = None # type: Any - self.entity_filter = generate_filter(include.get(CONF_DOMAINS, []), - include.get(CONF_ENTITIES, []), - exclude.get(CONF_DOMAINS, []), - exclude.get(CONF_ENTITIES, [])) + self.entity_filter = generate_filter( + include.get(CONF_DOMAINS, []), include.get(CONF_ENTITIES, []), + exclude.get(CONF_DOMAINS, []), exclude.get(CONF_ENTITIES, [])) self.exclude_t = exclude.get(CONF_EVENT_TYPES, []) self.get_session = None @@ -238,8 +237,7 @@ class Recorder(threading.Thread): self.queue.put(None) self.join() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - shutdown) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) if self.hass.state == CoreState.running: hass_started.set_result(None) @@ -249,8 +247,8 @@ class Recorder(threading.Thread): """Notify that hass has started.""" hass_started.set_result(None) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, - notify_hass_started) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, notify_hass_started) if self.keep_days and self.purge_interval: @callback diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 4ff8e239352..fad6a7de70d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -12,7 +12,8 @@ _LOGGER = logging.getLogger(__name__) def purge_old_data(instance, purge_days): """Purge events and states older than purge_days ago.""" from .models import States, Events - from sqlalchemy import func + from sqlalchemy import orm + from sqlalchemy.sql import exists purge_before = dt_util.utcnow() - timedelta(days=purge_days) @@ -20,12 +21,18 @@ def purge_old_data(instance, purge_days): # For each entity, the most recent state is protected from deletion # s.t. we can properly restore state even if the entity has not been # updated in a long time - protected_states = session.query(States.state_id, States.event_id, - func.max(States.last_updated)) \ - .group_by(States.entity_id).all() + states_alias = orm.aliased(States, name='StatesAlias') + protected_states = session.query(States.state_id, States.event_id)\ + .filter(~exists() + .where(States.entity_id == + states_alias.entity_id) + .where(states_alias.last_updated > + States.last_updated))\ + .all() protected_state_ids = tuple((state[0] for state in protected_states)) - protected_event_ids = tuple((state[1] for state in protected_states)) + protected_event_ids = tuple((state[1] for state in protected_states + if state[1] is not None)) deleted_rows = session.query(States) \ .filter((States.last_updated < purge_before)) \ diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 08c371fcf0a..98cd937de3c 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -1,21 +1,17 @@ -"""Component to interact with Remember The Milk. +""" +Component to interact with Remember The Milk. For more details about this component, please refer to the documentation at https://home-assistant.io/components/remember_the_milk/ - -Minimum viable product, it currently only support creating new tasks in your -Remember The Milk (https://www.rememberthemilk.com/) account. - -This product uses the Remember The Milk API but is not endorsed or certified -by Remember The Milk. """ +import json import logging import os -import json + import voluptuous as vol -from homeassistant.const import (CONF_API_KEY, STATE_OK, CONF_TOKEN, - CONF_NAME, CONF_ID) +from homeassistant.const import ( + CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN, STATE_OK) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -61,9 +57,9 @@ SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({ def setup(hass, config): - """Set up the remember_the_milk component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, - group_name=GROUP_NAME_RTM) + """Set up the Remember the milk component.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_RTM) stored_rtm_config = RememberTheMilkConfiguration(hass) for rtm_config in config[DOMAIN]: @@ -90,7 +86,7 @@ def _create_instance(hass, account_name, api_key, shared_secret, token, stored_rtm_config, component): entity = RememberTheMilk(account_name, api_key, shared_secret, token, stored_rtm_config) - component.add_entity(entity) + component.add_entities([entity]) hass.services.register( DOMAIN, '{}_create_task'.format(account_name), entity.create_task, schema=SERVICE_SCHEMA_CREATE_TASK) @@ -107,21 +103,21 @@ def _register_new_account(hass, account_name, api_key, shared_secret, configurator = hass.components.configurator api = Rtm(api_key, shared_secret, "write", None) url, frob = api.authenticate_desktop() - _LOGGER.debug('sent authentication request to server') + _LOGGER.debug("Sent authentication request to server") def register_account_callback(_): - """Callback for configurator.""" + """Call for register the configurator.""" api.retrieve_token(frob) token = api.token if api.token is None: - _LOGGER.error('Failed to register, please try again.') + _LOGGER.error("Failed to register, please try again") configurator.notify_errors( request_id, 'Failed to register, please try again.') return stored_rtm_config.set_token(account_name, token) - _LOGGER.debug('retrieved new token from server') + _LOGGER.debug("Retrieved new token from server") _create_instance( hass, account_name, api_key, shared_secret, token, @@ -155,13 +151,13 @@ class RememberTheMilkConfiguration(object): self._config = dict() return try: - _LOGGER.debug('loading configuration from file: %s', + _LOGGER.debug("Loading configuration from file: %s", self._config_file_path) with open(self._config_file_path, 'r') as config_file: self._config = json.load(config_file) except ValueError: - _LOGGER.error('failed to load configuration file, creating a ' - 'new one: %s', self._config_file_path) + _LOGGER.error("Failed to load configuration file, creating a " + "new one: %s", self._config_file_path) self._config = dict() def save_config(self): @@ -197,9 +193,9 @@ class RememberTheMilkConfiguration(object): self._config[profile_name][CONF_ID_MAP] = dict() def get_rtm_id(self, profile_name, hass_id): - """Get the rtm ids for a home assistant task id. + """Get the RTM ids for a Home Assistant task ID. - The id of a rtm tasks consists of the tuple: + The id of a RTM tasks consists of the tuple: list id, timeseries id and the task id. """ self._initialize_profile(profile_name) @@ -210,7 +206,7 @@ class RememberTheMilkConfiguration(object): def set_rtm_id(self, profile_name, hass_id, list_id, time_series_id, rtm_task_id): - """Add/Update the rtm task id for a home assistant task id.""" + """Add/Update the RTM task ID for a Home Assistant task IS.""" self._initialize_profile(profile_name) id_tuple = { CONF_LIST_ID: list_id, @@ -229,7 +225,7 @@ class RememberTheMilkConfiguration(object): class RememberTheMilk(Entity): - """MVP implementation of an interface to Remember The Milk.""" + """Representation of an interface to Remember The Milk.""" def __init__(self, name, api_key, shared_secret, token, rtm_config): """Create new instance of Remember The Milk component.""" @@ -243,7 +239,7 @@ class RememberTheMilk(Entity): self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token) self._token_valid = None self._check_token() - _LOGGER.debug("instance created for account %s", self._name) + _LOGGER.debug("Instance created for account %s", self._name) def _check_token(self): """Check if the API token is still valid. @@ -253,8 +249,8 @@ class RememberTheMilk(Entity): """ valid = self._rtm_api.token_valid() if not valid: - _LOGGER.error('Token for account %s is invalid. You need to ' - 'register again!', self.name) + _LOGGER.error("Token for account %s is invalid. You need to " + "register again!", self.name) self._rtm_config.delete_token(self._name) self._token_valid = False else: @@ -264,7 +260,7 @@ class RememberTheMilk(Entity): def create_task(self, call): """Create a new task on Remember The Milk. - You can use the smart syntax to define the attribues of a new task, + You can use the smart syntax to define the attributes of a new task, e.g. "my task #some_tag ^today" will add tag "some_tag" and set the due date to today. """ @@ -282,25 +278,20 @@ class RememberTheMilk(Entity): if hass_id is None or rtm_id is None: result = self._rtm_api.rtm.tasks.add( timeline=timeline, name=task_name, parse='1') - _LOGGER.debug('created new task "%s" in account %s', + _LOGGER.debug("Created new task '%s' in account %s", task_name, self.name) - self._rtm_config.set_rtm_id(self._name, - hass_id, - result.list.id, - result.list.taskseries.id, - result.list.taskseries.task.id) + self._rtm_config.set_rtm_id( + self._name, hass_id, result.list.id, + result.list.taskseries.id, result.list.taskseries.task.id) else: - self._rtm_api.rtm.tasks.setName(name=task_name, - list_id=rtm_id[0], - taskseries_id=rtm_id[1], - task_id=rtm_id[2], - timeline=timeline) - _LOGGER.debug('updated task with id "%s" in account ' - '%s to name %s', - hass_id, self.name, task_name) + self._rtm_api.rtm.tasks.setName( + name=task_name, list_id=rtm_id[0], taskseries_id=rtm_id[1], + task_id=rtm_id[2], timeline=timeline) + _LOGGER.debug("Updated task with id '%s' in account " + "%s to name %s", hass_id, self.name, task_name) except rtmapi.RtmRequestFailedException as rtm_exception: - _LOGGER.error('Error creating new Remember The Milk task for ' - 'account %s: %s', self._name, rtm_exception) + _LOGGER.error("Error creating new Remember The Milk task for " + "account %s: %s", self._name, rtm_exception) return False return True @@ -311,23 +302,21 @@ class RememberTheMilk(Entity): hass_id = call.data.get(CONF_ID) rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) if rtm_id is None: - _LOGGER.error('Could not find task with id %s in account %s. ' - 'So task could not be closed.', - hass_id, self._name) + _LOGGER.error("Could not find task with ID %s in account %s. " + "So task could not be closed", hass_id, self._name) return False try: result = self._rtm_api.rtm.timelines.create() timeline = result.timeline.value - self._rtm_api.rtm.tasks.complete(list_id=rtm_id[0], - taskseries_id=rtm_id[1], - task_id=rtm_id[2], - timeline=timeline) + self._rtm_api.rtm.tasks.complete( + list_id=rtm_id[0], taskseries_id=rtm_id[1], task_id=rtm_id[2], + timeline=timeline) self._rtm_config.delete_rtm_id(self._name, hass_id) - _LOGGER.debug('Completed task with id %s in account %s', + _LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name) except rtmapi.RtmRequestFailedException as rtm_exception: - _LOGGER.error('Error creating new Remember The Milk task for ' - 'account %s: %s', self._name, rtm_exception) + _LOGGER.error("Error creating new Remember The Milk task for " + "account %s: %s", self._name, rtm_exception) return True @property @@ -339,5 +328,5 @@ class RememberTheMilk(Entity): def state(self): """Return the state of the device.""" if not self._token_valid: - return 'API token invalid' + return "API token invalid" return STATE_OK diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 4d241ed5913..89cdc7529cb 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -4,19 +4,19 @@ Support for Harmony Hub devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.harmony/ """ -import logging import asyncio +import logging import time import voluptuous as vol import homeassistant.components.remote as remote -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.remote import ( - PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_ACTIVITY, ATTR_NUM_REPEATS, - ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify REQUIREMENTS = ['pyharmony==1.0.18'] @@ -30,12 +30,12 @@ CONF_DEVICE_CACHE = 'harmony_device_cache' SERVICE_SYNC = 'harmony_sync' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(ATTR_ACTIVITY, default=None): cv.string, + vol.Required(CONF_NAME): cv.string, vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) HARMONY_SYNC_SCHEMA = vol.Schema({ @@ -182,7 +182,7 @@ class HarmonyRemote(remote.RemoteDevice): return self._current_activity not in [None, 'PowerOff'] def new_activity(self, activity_id): - """Callback for updating the current activity.""" + """Call for updating the current activity.""" import pyharmony activity_name = pyharmony.activity_name(self._config, activity_id) _LOGGER.debug("%s activity reported as: %s", self._name, activity_name) diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py index c1a04718d33..7a04949dbeb 100644 --- a/homeassistant/components/remote/kira.py +++ b/homeassistant/components/remote/kira.py @@ -4,15 +4,13 @@ Support for Keene Electronics IR-IP devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.kira/ """ -import logging import functools as ft +import logging import homeassistant.components.remote as remote +from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.helpers.entity import Entity -from homeassistant.const import ( - CONF_DEVICE, CONF_NAME) - DOMAIN = 'kira' _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 4994e333eda..7d2e428c56b 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -39,7 +39,6 @@ CONF_AUTOMATIC_ADD = 'automatic_add' CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' -CONF_DATA_BITS = 'data_bits' CONF_DUMMY = 'dummy' CONF_DEVICE = 'device' CONF_DEBUG = 'debug' diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index f035ae3128e..067db1f93a3 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -4,7 +4,6 @@ Support for deCONZ scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ - import asyncio from homeassistant.components.deconz import DOMAIN as DECONZ_DATA @@ -31,7 +30,7 @@ class DeconzScene(Scene): """Representation of a deCONZ scene.""" def __init__(self, scene): - """Setup scene.""" + """Set up a scene.""" self._scene = scene @asyncio.coroutine diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py index 008edf6f131..2d4a6d0621c 100644 --- a/homeassistant/components/scene/wink.py +++ b/homeassistant/components/scene/wink.py @@ -8,10 +8,11 @@ import asyncio import logging from homeassistant.components.scene import Scene -from homeassistant.components.wink import WinkDevice, DOMAIN +from homeassistant.components.wink import DOMAIN, WinkDevice + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['wink'] -_LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -34,7 +35,7 @@ class WinkScene(WinkDevice, Scene): @asyncio.coroutine def async_added_to_hass(self): - """Callback when entity is added to hass.""" + """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['scene'].append(self) @property diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 7be8bd8175e..a45f8ba8930 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -156,7 +156,7 @@ def _async_process_config(hass, config, component): def service_handler(service): """Execute a service call to script.