diff --git a/.coveragerc b/.coveragerc index 80ca261f32d..dfbbb232efc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,9 @@ omit = homeassistant/components/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py + homeassistant/components/hydrawise.py + homeassistant/components/*/hydrawise.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py @@ -216,7 +219,7 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py - homeassistant/components/rainmachine.py + homeassistant/components/rainmachine/* homeassistant/components/*/rainmachine.py homeassistant/components/raspihats.py @@ -382,6 +385,7 @@ omit = homeassistant/components/cover/myq.py homeassistant/components/cover/opengarage.py homeassistant/components/cover/rpi_gpio.py + homeassistant/components/cover/ryobi_gdo.py homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py @@ -443,6 +447,7 @@ omit = homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py + homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py @@ -518,9 +523,10 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend.py + homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/discord.py + homeassistant/components/notify/flock.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py @@ -533,7 +539,6 @@ omit = homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py - homeassistant/components/notify/nma.py homeassistant/components/notify/prowl.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py @@ -620,6 +625,7 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py + homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a8e6812cf3..c2f65f9a8be 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed: If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. + - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: diff --git a/CODEOWNERS b/CODEOWNERS index 32639fed43c..0da8353e5aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git -homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi @@ -100,6 +99,8 @@ homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/rainmachine/* @bachya +homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei diff --git a/Dockerfile b/Dockerfile index 5081b4ba721..75d9e9eb716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 488d6bddd46..00000000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,606 +0,0 @@ -swagger: '2.0' -info: - title: Home Assistant - description: Home Assistant REST API - version: "1.0.1" -# the domain of the service -host: localhost:8123 - -# array of all schemes that your API supports -schemes: - - http - - https - -securityDefinitions: - #api_key: - # type: apiKey - # description: API password - # name: api_password - # in: query - - api_key: - type: apiKey - description: API password - name: x-ha-access - in: header - -# will be prefixed to all paths -basePath: /api - -consumes: - - application/json -produces: - - application/json -paths: - /: - get: - summary: API alive message - description: Returns message if API is up and running. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: API is up and running - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /config: - get: - summary: API alive message - description: Returns the current configuration as JSON. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Current configuration - schema: - $ref: '#/definitions/ApiConfig' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /discovery_info: - get: - summary: Basic information about Home Assistant instance - tags: - - Core - responses: - 200: - description: Basic information - schema: - $ref: '#/definitions/DiscoveryInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /bootstrap: - get: - summary: Returns all data needed to bootstrap Home Assistant. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Bootstrap information - schema: - $ref: '#/definitions/BootstrapInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events: - get: - summary: Array of event objects. - description: Returns an array of event objects. Each event object contain event name and listener count. - tags: - - Events - security: - - api_key: [] - responses: - 200: - description: Events - schema: - type: array - items: - $ref: '#/definitions/Event' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services: - get: - summary: Array of service objects. - description: Returns an array of service objects. Each object contains the domain and which services it contains. - tags: - - Services - security: - - api_key: [] - responses: - 200: - description: Services - schema: - type: array - items: - $ref: '#/definitions/Service' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /history: - get: - summary: Array of state changes in the past. - description: Returns an array of state changes in the past. Each object contains further detail for the entities. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: State changes - schema: - type: array - items: - $ref: '#/definitions/History' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states: - get: - summary: Array of state objects. - description: | - Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: States - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states/{entity_id}: - get: - summary: Specific state object. - description: | - Returns a state object for specified entity_id. - tags: - - State - security: - - api_key: [] - parameters: - - name: entity_id - in: path - description: entity_id of the entity to query - required: true - type: string - responses: - 200: - description: State - schema: - $ref: '#/definitions/State' - 404: - description: Not found - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - post: - description: | - Updates or creates the current state of an entity. - tags: - - State - consumes: - - application/json - parameters: - - name: entity_id - in: path - description: entity_id to set the state of - required: true - type: string - - $ref: '#/parameters/State' - responses: - 200: - description: State of existing entity was set - schema: - $ref: '#/definitions/State' - 201: - description: State of new entity was set - schema: - $ref: '#/definitions/State' - headers: - location: - type: string - description: location of the new entity - default: - description: Error - schema: - $ref: '#/definitions/Message' - /error_log: - get: - summary: Error log - description: | - Retrieve all errors logged during the current session of Home Assistant as a plaintext response. - tags: - - Core - security: - - api_key: [] - produces: - - text/plain - responses: - 200: - description: Plain text error log - default: - description: Error - schema: - $ref: '#/definitions/Message' - /camera_proxy/camera.{entity_id}: - get: - summary: Camera image. - description: | - Returns the data (image) from the specified camera entity_id. - tags: - - Camera - security: - - api_key: [] - produces: - - image/jpeg - parameters: - - name: entity_id - in: path - description: entity_id of the camera to query - required: true - type: string - responses: - 200: - description: Camera image - schema: - type: file - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events/{event_type}: - post: - description: | - Fires an event with event_type - tags: - - Events - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: event_type - in: path - description: event_type to fire event with - required: true - type: string - - $ref: '#/parameters/EventData' - responses: - 200: - description: Response message - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services/{domain}/{service}: - post: - description: | - Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first. - tags: - - Services - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: domain - in: path - description: domain of the service - required: true - type: string - - name: service - in: path - description: service to call - required: true - type: string - - $ref: '#/parameters/ServiceData' - responses: - 200: - description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system. - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /template: - post: - description: | - Render a Home Assistant template. - tags: - - Template - security: - - api_key: [] - consumes: - - application/json - produces: - - text/plain - parameters: - - $ref: '#/parameters/Template' - responses: - 200: - description: Returns the rendered template in plain text. - schema: - type: string - default: - description: Error - schema: - $ref: '#/definitions/Message' - /event_forwarding: - post: - description: | - Setup event forwarding to another Home Assistant instance. - tags: - - Core - security: - - api_key: [] - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was setup successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - delete: - description: | - Cancel event forwarding to another Home Assistant instance. - tags: - - Core - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was cancelled successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /stream: - get: - summary: Server-sent events - description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer. - tags: - - Core - - Events - security: - - api_key: [] - produces: - - text/event-stream - parameters: - - name: restrict - in: query - description: comma-separated list of event_types to filter - required: false - type: string - responses: - default: - description: Stream of events - schema: - type: object - x-events: - state_changed: - type: object - properties: - entity_id: - type: string - old_state: - $ref: '#/definitions/State' - new_state: - $ref: '#/definitions/State' -definitions: - ApiConfig: - type: object - properties: - components: - type: array - description: List of component types - items: - type: string - description: Component type - latitude: - type: number - format: float - description: Latitude of Home Assistant server - longitude: - type: number - format: float - description: Longitude of Home Assistant server - location_name: - type: string - unit_system: - type: object - properties: - length: - type: string - mass: - type: string - temperature: - type: string - volume: - type: string - time_zone: - type: string - version: - type: string - DiscoveryInfo: - type: object - properties: - base_url: - type: string - location_name: - type: string - requires_api_password: - type: boolean - version: - type: string - BootstrapInfo: - type: object - properties: - config: - $ref: '#/definitions/ApiConfig' - events: - type: array - items: - $ref: '#/definitions/Event' - services: - type: array - items: - $ref: '#/definitions/Service' - states: - type: array - items: - $ref: '#/definitions/State' - Event: - type: object - properties: - event: - type: string - listener_count: - type: integer - Service: - type: object - properties: - domain: - type: string - services: - type: object - additionalProperties: - $ref: '#/definitions/DomainService' - DomainService: - type: object - properties: - description: - type: string - fields: - type: object - description: Object with service fields that can be called - State: - type: object - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - entity_id: - type: string - last_changed: - type: string - format: date-time - StateAttributes: - type: object - additionalProperties: - type: string - History: - allOf: - - $ref: '#/definitions/State' - - type: object - properties: - last_updated: - type: string - format: date-time - Message: - type: object - properties: - message: - type: string -parameters: - State: - name: body - in: body - description: State parameter - required: false - schema: - type: object - required: - - state - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - EventData: - name: body - in: body - description: event_data - required: false - schema: - type: object - ServiceData: - name: body - in: body - description: service_data - required: false - schema: - type: object - Template: - name: body - in: body - description: Template to render - required: true - schema: - type: object - required: - - template - properties: - template: - description: Jinja2 template string - type: string - EventForwarding: - name: body - in: body - description: Event Forwarding parameter - required: true - schema: - type: object - required: - - host - - api_password - properties: - host: - type: string - api_password: - type: string - port: - type: integer diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 49df9f2cefa..626022e362a 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + """Return one or more digits/characters.""" + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 31d93373286..87e85f09da0 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ import asyncio import logging +import re import voluptuous as vol @@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index d48a107f33d..9a65fdaff06 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index e5003f1ba1d..25224484c79 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return '^\\d{4,6}$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 7bdc1ccd9d9..209c5367c92 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.ifttt/ """ import logging +import re import voluptuous as vol @@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 5beb5261607..2f2f89b9dfc 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/ import copy import datetime import logging +import re import voluptuous as vol @@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 4b08ad67292..895f5edd5da 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -8,6 +8,7 @@ import asyncio import copy import datetime import logging +import re import voluptuous as vol @@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 1422136c405..8a0dfefdc70 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import asyncio import logging +import re import voluptuous as vol @@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): @property def code_format(self): - """One or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index ceb79c1dc7b..ca6f1a44a6f 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return che characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + """Return one or more digits/characters.""" + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 964047f91e9..4ac3a93fff4 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 3b991c5b236..b4906acba3c 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.simplisafe/ """ import logging +import re import voluptuous as vol @@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 1f383e32f92..674eac97f8c 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.17'] +REQUIREMENTS = ['total_connect_client==0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 74d63b1fb9c..59bfe15fa9b 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return the code format as regex.""" - return '^\\d{%s}$' % self._digits + """Return one or more digits/characters.""" + return 'Number' @property def changed_by(self): diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index dc34006ad03..ae89e2fc3b6 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -2,7 +2,7 @@ Rest API for Home Assistant. For more details about the RESTful API, please refer to the documentation at -https://home-assistant.io/developers/api/ +https://developers.home-assistant.io/docs/en/external_api_rest.html """ import asyncio import json @@ -11,31 +11,34 @@ import logging from aiohttp import web import async_timeout -import homeassistant.core as ha -import homeassistant.remote as rem from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, - HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, - MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, - URL_API_EVENTS, URL_API_SERVICES, - URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, - __version__) -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, + HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, + URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, + URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, + URL_API_TEMPLATE, __version__) +import homeassistant.core as ha +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.state import AsyncTrackStates +import homeassistant.remote as rem + +_LOGGER = logging.getLogger(__name__) + +ATTR_BASE_URL = 'base_url' +ATTR_LOCATION_NAME = 'location_name' +ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' +ATTR_VERSION = 'version' DOMAIN = 'api' DEPENDENCIES = ['http'] -STREAM_PING_PAYLOAD = "ping" +STREAM_PING_PAYLOAD = 'ping' STREAM_PING_INTERVAL = 50 # seconds -_LOGGER = logging.getLogger(__name__) - def setup(hass, config): """Register the API with the HTTP interface.""" @@ -62,19 +65,19 @@ class APIStatusView(HomeAssistantView): """View to handle Status requests.""" url = URL_API - name = "api:status" + name = 'api:status' @ha.callback def get(self, request): """Retrieve if API is running.""" - return self.json_message('API running.') + return self.json_message("API running.") class APIEventStream(HomeAssistantView): """View to handle EventStream requests.""" url = URL_API_STREAM - name = "api:stream" + name = 'api:stream' async def get(self, request): """Provide a streaming interface for the event bus.""" @@ -95,7 +98,7 @@ class APIEventStream(HomeAssistantView): if restrict and event.event_type not in restrict: return - _LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event) + _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) if event.event_type == EVENT_HOMEASSISTANT_STOP: data = stop_obj @@ -111,7 +114,7 @@ class APIEventStream(HomeAssistantView): unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) try: - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) # Fire off one message so browsers fire open event right away await to_write.put(STREAM_PING_PAYLOAD) @@ -126,25 +129,25 @@ class APIEventStream(HomeAssistantView): break msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - await response.write(msg.encode("UTF-8")) + _LOGGER.debug( + "STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode('UTF-8')) except asyncio.TimeoutError: await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: - _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) + _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) finally: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) unsub_stream() class APIConfigView(HomeAssistantView): - """View to handle Config requests.""" + """View to handle Configuration requests.""" url = URL_API_CONFIG - name = "api:config" + name = 'api:config' @ha.callback def get(self, request): @@ -153,22 +156,22 @@ class APIConfigView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView): - """View to provide discovery info.""" + """View to provide Discovery information.""" requires_auth = False url = URL_API_DISCOVERY_INFO - name = "api:discovery" + name = 'api:discovery' @ha.callback def get(self, request): - """Get discovery info.""" + """Get discovery information.""" hass = request.app['hass'] needs_auth = hass.config.api.api_password is not None return self.json({ - 'base_url': hass.config.api.base_url, - 'location_name': hass.config.location_name, - 'requires_api_password': needs_auth, - 'version': __version__ + ATTR_BASE_URL: hass.config.api.base_url, + ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_REQUIRES_API_PASSWORD: needs_auth, + ATTR_VERSION: __version__, }) @@ -187,8 +190,8 @@ class APIStatesView(HomeAssistantView): class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = "/api/states/{entity_id}" - name = "api:entity-state" + url = '/api/states/{entity_id}' + name = 'api:entity-state' @ha.callback def get(self, request, entity_id): @@ -196,7 +199,7 @@ class APIEntityStateView(HomeAssistantView): state = request.app['hass'].states.get(entity_id) if state: return self.json(state) - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity not found.", HTTP_NOT_FOUND) async def post(self, request, entity_id): """Update state of entity.""" @@ -204,13 +207,13 @@ class APIEntityStateView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON specified', - HTTP_BAD_REQUEST) + return self.json_message( + "Invalid JSON specified.", HTTP_BAD_REQUEST) new_state = data.get('state') if new_state is None: - return self.json_message('No state specified', HTTP_BAD_REQUEST) + return self.json_message("No state specified.", HTTP_BAD_REQUEST) attributes = data.get('attributes') force_update = data.get('force_update', False) @@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView): def delete(self, request, entity_id): """Remove entity.""" if request.app['hass'].states.async_remove(entity_id): - return self.json_message('Entity removed') - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity removed.") + return self.json_message("Entity not found.", HTTP_NOT_FOUND) class APIEventListenersView(HomeAssistantView): """View to handle EventListeners requests.""" url = URL_API_EVENTS - name = "api:event-listeners" + name = 'api:event-listeners' @ha.callback def get(self, request): @@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView): """View to handle Event requests.""" url = '/api/events/{event_type}' - name = "api:event" + name = 'api:event' async def post(self, request, event_type): """Fire events.""" @@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView): try: event_data = json.loads(body) if body else None except ValueError: - return self.json_message('Event data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be valid JSON.", HTTP_BAD_REQUEST) if event_data is not None and not isinstance(event_data, dict): - return self.json_message('Event data should be a JSON object', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be a JSON object", HTTP_BAD_REQUEST) # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects @@ -276,8 +279,8 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - request.app['hass'].bus.async_fire(event_type, event_data, - ha.EventOrigin.remote) + request.app['hass'].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -286,7 +289,7 @@ class APIServicesView(HomeAssistantView): """View to handle Services requests.""" url = URL_API_SERVICES - name = "api:services" + name = 'api:services' async def get(self, request): """Get registered services.""" @@ -297,8 +300,8 @@ class APIServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = "/api/services/{domain}/{service}" - name = "api:domain-services" + url = '/api/services/{domain}/{service}' + name = 'api:domain-services' async def post(self, request, domain, service): """Call a service. @@ -310,8 +313,8 @@ class APIDomainServicesView(HomeAssistantView): try: data = json.loads(body) if body else None except ValueError: - return self.json_message('Data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Data should be valid JSON.", HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: await hass.services.async_call(domain, service, data, True) @@ -323,7 +326,7 @@ class APIComponentsView(HomeAssistantView): """View to handle Components requests.""" url = URL_API_COMPONENTS - name = "api:components" + name = 'api:components' @ha.callback def get(self, request): @@ -332,10 +335,10 @@ class APIComponentsView(HomeAssistantView): class APITemplateView(HomeAssistantView): - """View to handle requests.""" + """View to handle Template requests.""" url = URL_API_TEMPLATE - name = "api:template" + name = 'api:template' async def post(self, request): """Render a template.""" @@ -344,30 +347,29 @@ class APITemplateView(HomeAssistantView): tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: - return self.json_message('Error rendering template: {}'.format(ex), - HTTP_BAD_REQUEST) + return self.json_message( + "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) class APIErrorLog(HomeAssistantView): - """View to fetch the error log.""" + """View to fetch the API error log.""" url = URL_API_ERROR_LOG - name = "api:error_log" + name = 'api:error_log' async def get(self, request): """Retrieve API error log.""" - return web.FileResponse( - request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): """Generate services data to JSONify.""" descriptions = await async_get_all_descriptions(hass) - return [{"domain": key, "services": value} + return [{'domain': key, 'services': value} for key, value in descriptions.items()] def async_events_json(hass): """Generate event data to JSONify.""" - return [{"event": key, "listener_count": value} + return [{'event': key, 'listener_count': value} for key, value in hass.bus.async_listeners().items()] diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index a9bd5c9c8bc..68445092db7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -17,7 +17,7 @@ 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'] +REQUIREMENTS = ['pyatv==0.3.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 2f510fd33d6..2a7a3887b34 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({ }) TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_VARIABLES, default={}): dict, }) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 9faa703d13c..6f59da0755a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add binary sensor from deCONZ.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_BINARY_SENSOR: + if sensor.type in DECONZ_BINARY_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.type in PRESENCE and self._sensor.dark: + if self._sensor.type in PRESENCE and self._sensor.dark is not None: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 0aadcc247ea..f358f814dc5 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/ """ import asyncio import logging +import datetime from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,6 +15,7 @@ from homeassistant.components.envisalink import ( DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, SIGNAL_ZONE_UPDATE) from homeassistant.const import ATTR_LAST_TRIP_TIME +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): def device_state_attributes(self): """Return the state attributes.""" attr = {} - attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault'] + + # The Envisalink library returns a "last_fault" value that's the + # number of seconds since the last fault, up to a maximum of 327680 + # seconds (65536 5-second ticks). + # + # We don't want the HA event log to fill up with a bunch of no-op + # "state changes" that are just that number ticking up once per poll + # interval, so we subtract it from the current second-accurate time + # unless it is already at the maximum value, in which case we set it + # to None since we can't determine the actual value. + seconds_ago = self._info['last_fault'] + if seconds_ago < 65536 * 5: + now = dt_util.now().replace(microsecond=0) + delta = datetime.timedelta(seconds=seconds_ago) + last_trip_time = (now - delta).isoformat() + else: + last_trip_time = None + + attr[ATTR_LAST_TRIP_TIME] = last_trip_time return attr @property diff --git a/homeassistant/components/binary_sensor/hydrawise.py b/homeassistant/components/binary_sensor/hydrawise.py new file mode 100644 index 00000000000..a3e0ebd782d --- /dev/null +++ b/homeassistant/components/binary_sensor/hydrawise.py @@ -0,0 +1,81 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, + DEVICE_MAP_INDEX) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in ['status', 'rain_sensor']: + sensors.append( + HydrawiseBinarySensor( + hydrawise.controller_status, sensor_type)) + + else: + # create a sensor for each zone + for zone in hydrawise.relays: + zone_data = zone + zone_data['running'] = \ + hydrawise.controller_status.get('running', False) + sensors.append(HydrawiseBinarySensor(zone_data, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): + """A sensor implementation for Hydrawise device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name) + mydata = self.hass.data[DATA_HYDRAWISE].data + if self._sensor_type == 'status': + self._state = mydata.status == 'All good!' + elif self._sensor_type == 'rain_sensor': + for sensor in mydata.sensors: + if sensor['name'] == 'Rain': + self._state = sensor['active'] == 1 + elif self._sensor_type == 'is_watering': + if not mydata.running: + self._state = False + elif int(mydata.running[0]['relay']) == self.data['relay']: + self._state = True + else: + self._state = False + + @property + def device_class(self): + """Return the device class of the sensor type.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')] diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 4089f3a2eaf..882ff142e8c 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -7,27 +7,36 @@ https://home-assistant.io/components/binary_sensor.nest/ from itertools import chain import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.sensor.nest import NestSensor +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.components.nest import DATA_NEST DEPENDENCIES = ['nest'] -BINARY_TYPES = ['online'] +BINARY_TYPES = {'online': 'connectivity'} -CLIMATE_BINARY_TYPES = [ - 'fan', - 'is_using_emergency_heat', - 'is_locked', - 'has_leaf', -] +CLIMATE_BINARY_TYPES = { + 'fan': None, + 'is_using_emergency_heat': 'heat', + 'is_locked': None, + 'has_leaf': None, +} -CAMERA_BINARY_TYPES = [ - 'motion_detected', - 'sound_detected', - 'person_detected', -] +CAMERA_BINARY_TYPES = { + 'motion_detected': 'motion', + 'sound_detected': 'sound', + 'person_detected': 'occupancy', +} + +STRUCTURE_BINARY_TYPES = { + 'away': None, + # 'security_state', # pending python-nest update +} + +STRUCTURE_BINARY_STATE_MAP = { + 'away': {'away': True, 'home': False}, + 'security_state': {'deter': True, 'ok': False}, +} _BINARY_TYPES_DEPRECATED = [ 'hvac_ac_state', @@ -40,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [ 'hvac_emer_heat_state', ] -_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ - + CAMERA_BINARY_TYPES +_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES} _LOGGER = logging.getLogger(__name__) @@ -68,6 +77,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(wstr) sensors = [] + for structure in nest.structures(): + sensors += [NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES] device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) @@ -88,11 +101,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors += [NestActivityZoneSensor(structure, device, activity_zone)] - add_devices(sensors, True) -class NestBinarySensor(NestSensor, BinarySensorDevice): +class NestBinarySensor(NestSensorDevice, BinarySensorDevice): """Represents a Nest binary sensor.""" @property @@ -100,9 +112,19 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + def update(self): """Retrieve latest state.""" - self._state = bool(getattr(self.device, self.variable)) + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP + [self.variable][value]) + else: + self._state = bool(value) class NestActivityZoneSensor(NestBinarySensor): @@ -115,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor): self._name = "{} {} activity".format(self._name, self.zone.name) @property - def name(self): - """Return the name of the nest, if any.""" - return self._name + def device_class(self): + """Return the device class of the binary sensor.""" + return 'motion' def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py new file mode 100644 index 00000000000..601a73298af --- /dev/null +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -0,0 +1,102 @@ +""" +This platform provides binary sensors for key RainMachine data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rainmachine/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rainmachine import ( + BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, + TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + binary_sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon = BINARY_SENSORS[sensor_type] + binary_sensors.append( + RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) + + add_devices(binary_sensors, True) + + +class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon): + """Initialize the sensor.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the state.""" + if self._sensor_type == TYPE_FREEZE: + self._state = self.rainmachine.restrictions['current']['freeze'] + elif self._sensor_type == TYPE_FREEZE_PROTECTION: + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectEnabled'] + elif self._sensor_type == TYPE_HOT_DAYS: + self._state = self.rainmachine.restrictions['global'][ + 'hotDaysExtraWatering'] + elif self._sensor_type == TYPE_HOURLY: + self._state = self.rainmachine.restrictions['current']['hourly'] + elif self._sensor_type == TYPE_MONTH: + self._state = self.rainmachine.restrictions['current']['month'] + elif self._sensor_type == TYPE_RAINDELAY: + self._state = self.rainmachine.restrictions['current']['rainDelay'] + elif self._sensor_type == TYPE_RAINSENSOR: + self._state = self.rainmachine.restrictions['current'][ + 'rainSensor'] + elif self._sensor_type == TYPE_WEEKDAY: + self._state = self.rainmachine.restrictions['current']['weekDay'] diff --git a/homeassistant/components/binary_sensor/random.py b/homeassistant/components/binary_sensor/random.py index 162d0480389..ab6c1e5d479 100644 --- a/homeassistant/components/binary_sensor/random.py +++ b/homeassistant/components/binary_sensor/random.py @@ -4,7 +4,6 @@ Support for showing random states. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Random binary sensor.""" name = config.get(CONF_NAME) device_class = config.get(CONF_DEVICE_CLASS) @@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get new state and update the sensor's state.""" from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 1c0b903d868..72a4cfdfbaa 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -330,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor): click_type = 'both' elif value == 'shake': click_type = 'shake' + elif value == 'long_click': + return False else: _LOGGER.warning("Unsupported click_type detected: %s", value) return False diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index d3b31188760..6931355ca0e 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -203,14 +203,19 @@ class Switch(zha.Entity, BinarySensorDevice): def __init__(self, **kwargs): """Initialize Switch.""" super().__init__(**kwargs) - self._state = True - self._level = 255 + self._state = False + self._level = 0 from zigpy.zcl.clusters import general self._out_listeners = { general.OnOff.cluster_id: self.OnOffListener(self), general.LevelControl.cluster_id: self.LevelListener(self), } + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 550d4035ddd..ebe7cbbf2c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -22,6 +22,12 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS, ) + +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 +DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MAX_HUMIDITY = 99 + DOMAIN = 'climate' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -778,19 +784,21 @@ class ClimateDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def min_humidity(self): """Return the minimum humidity.""" - return 30 + return DEFAULT_MIN_HUMITIDY @property def max_humidity(self): """Return the maximum humidity.""" - return 99 + return DEFAULT_MAX_HUMIDITY diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index b5d3c3f7c25..6b7f6cb2afc 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) 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, @@ -267,8 +268,7 @@ class GenericThermostat(ClimateDevice): if self._min_temp: return self._min_temp - # get default temp from super class - return ClimateDevice.min_temp.fget(self) + return DEFAULT_MIN_TEMP @property def max_temp(self): @@ -277,8 +277,7 @@ class GenericThermostat(ClimateDevice): if self._max_temp: return self._max_temp - # Get default temp from super class - return ClimateDevice.max_temp.fget(self) + return DEFAULT_MAX_TEMP @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/climate/homematicip_cloud.py new file mode 100644 index 00000000000..bf96f1f746d --- /dev/null +++ b/homeassistant/components/climate/homematicip_cloud.py @@ -0,0 +1,101 @@ +""" +Support for HomematicIP climate. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/climate.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.climate import ( + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE, + STATE_AUTO, STATE_MANUAL) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +_LOGGER = logging.getLogger(__name__) + +STATE_BOOST = 'Boost' + +HA_STATE_TO_HMIP = { + STATE_AUTO: 'AUTOMATIC', + STATE_MANUAL: 'MANUAL', +} + +HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()} + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP climate devices.""" + from homematicip.group import HeatingGroup + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + + devices = [] + for device in home.groups: + if isinstance(device, HeatingGroup): + devices.append(HomematicipHeatingGroup(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): + """Representation of a MomematicIP heating group.""" + + def __init__(self, home, device): + """Initialize heating group.""" + device.modelType = 'Group-Heating' + super().__init__(home, device) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._device.setPointTemperature + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.actualTemperature + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._device.humidity + + @property + def current_operation(self): + """Return current operation ie. automatic or manual.""" + return HMIP_STATE_TO_HA.get(self._device.controlMode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._device.minTemperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._device.maxTemperature + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._device.set_point_temperature(temperature) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 28e8020ab90..696f1479c08 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -18,6 +18,7 @@ from homeassistant.components.climate import ( from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['nest'] _LOGGER = logging.getLogger(__name__) @@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): temp_unit = hass.config.units.temperature_unit - add_devices( - [NestThermostat(structure, device, temp_unit) - for structure, device in hass.data[DATA_NEST].thermostats()], - True - ) + all_devices = [NestThermostat(structure, device, temp_unit) + for structure, device in hass.data[DATA_NEST].thermostats()] + + add_devices(all_devices, True) class NestThermostat(ClimateDevice): @@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice): self._min_temperature = None self._max_temperature = None + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update device state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + @property def supported_features(self): """Return the list of supported features.""" @@ -170,18 +184,24 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" import nest + temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) 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) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) else: temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) try: - self.device.target = temp - except nest.nest.APIError: - _LOGGER.error("An error occurred while setting the temperature") + if temp is not None: + self.device.target = temp + except nest.nest.APIError as api_error: + _LOGGER.error("An error occurred while setting temperature: %s", + api_error) + # restore target temperature + self.schedule_update_ha_state(True) def set_operation_mode(self, operation_mode): """Set operation mode.""" diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 2b92d050d3b..b3fff0dd796 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF) + SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -154,7 +154,8 @@ class SensiboClimate(ClimateDevice): @property def device_state_attributes(self): """Return the state attributes.""" - return {ATTR_CURRENT_HUMIDITY: self.current_humidity} + return {ATTR_CURRENT_HUMIDITY: self.current_humidity, + 'battery': self.current_battery} @property def temperature_unit(self): @@ -191,6 +192,11 @@ class SensiboClimate(ClimateDevice): """Return the current humidity.""" return self._measurements['humidity'] + @property + def current_battery(self): + """Return the current battery voltage.""" + return self._measurements.get('batteryVoltage') + @property def current_temperature(self): """Return the current temperature.""" @@ -240,13 +246,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if self._temperatures_list else super().min_temp + if self._temperatures_list else DEFAULT_MIN_TEMP @property def max_temp(self): """Return the maximum temperature.""" return self._temperatures_list[-1] \ - if self._temperatures_list else super().max_temp + if self._temperatures_list else DEFAULT_MAX_TEMP @property def unique_id(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 437c8ec3371..59da425553a 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -8,7 +8,8 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO @@ -232,16 +233,16 @@ class TadoClimate(ClimateDevice): """Return the minimum temperature.""" if self._min_temp: return self._min_temp - # get default temp from super class - return super().min_temp + + return DEFAULT_MIN_TEMP @property def max_temp(self): """Return the maximum temperature.""" if self._max_temp: return self._max_temp - # Get default temp from super class - return super().max_temp + + return DEFAULT_MAX_TEMP def update(self): """Update the state of this climate device.""" diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 7cf8e50e866..12b81c9003b 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -185,7 +185,7 @@ class CloudIoT: yield from client.send_json(response) except client_exceptions.WSServerHandshakeError as err: - if err.code == 401: + if err.status == 401: disconnect_warn = 'Invalid auth.' self.close_requested = True # Should we notify user? diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a8800d9583..b907d4b4217 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -21,7 +21,7 @@ ON_DEMAND = ('zwave',) async def async_setup(hass, config): """Set up the config component.""" await hass.components.frontend.async_register_built_in_panel( - 'config', 'config', 'mdi:settings') + 'config', 'config', 'hass:settings') async def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 2df17a4e50a..03e5b273468 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,9 +9,9 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -94,9 +94,8 @@ def async_reset(hass, entity_id): DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): - """Set up a counter.""" +async def async_setup(hass, config): + """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] @@ -115,8 +114,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the counter services.""" target_counters = component.async_extract_from_service(service) @@ -129,7 +127,7 @@ def async_setup(hass, config): tasks = [getattr(counter, attr)() for counter in target_counters] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_INCREMENT, async_handler_service) @@ -138,7 +136,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_RESET, async_handler_service) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -181,30 +179,26 @@ class Counter(Entity): ATTR_STEP: self._step, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_decrement(self): + async def async_decrement(self): """Decrement the counter.""" self._state -= self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_increment(self): + async def async_increment(self): """Increment a counter.""" self._state += self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_reset(self): + async def async_reset(self): """Reset a counter.""" self._state = self._initial - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index f07d3849fae..1e2ec43181c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -69,6 +69,11 @@ class MyQDevice(CoverDevice): self._name = device['name'] self._status = STATE_CLOSED + @property + def device_class(self): + """Define this cover as a garage door.""" + return 'garage' + @property def should_poll(self): """Poll for state.""" diff --git a/homeassistant/components/cover/ryobi_gdo.py b/homeassistant/components/cover/ryobi_gdo.py new file mode 100644 index 00000000000..a11d70dd3ad --- /dev/null +++ b/homeassistant/components/cover/ryobi_gdo.py @@ -0,0 +1,103 @@ +""" +Ryobi platform for the cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.ryobi_gdo/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED) + +REQUIREMENTS = ['py_ryobi_gdo==0.0.10'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEVICE_ID = 'device_id' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + +SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ryobi covers.""" + from py_ryobi_gdo import RyobiGDO as ryobi_door + covers = [] + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + devices = config.get(CONF_DEVICE_ID) + + for device_id in devices: + my_door = ryobi_door(username, password, device_id) + _LOGGER.debug("Getting the API key") + if my_door.get_api_key() is False: + _LOGGER.error("Wrong credentials, no API key retrieved") + return + _LOGGER.debug("Checking if the device ID is present") + if my_door.check_device_id() is False: + _LOGGER.error("%s not in your device list", device_id) + return + _LOGGER.debug("Adding device %s to covers", device_id) + covers.append(RyobiCover(hass, my_door)) + if covers: + _LOGGER.debug("Adding covers") + add_devices(covers, True) + + +class RyobiCover(CoverDevice): + """Representation of a ryobi cover.""" + + def __init__(self, hass, ryobi_door): + """Initialize the cover.""" + self.ryobi_door = ryobi_door + self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id()) + self._door_state = None + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._door_state == STATE_UNKNOWN: + return False + return self._door_state == STATE_CLOSED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'garage' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + def close_cover(self, **kwargs): + """Close the cover.""" + _LOGGER.debug("Closing garage door") + self.ryobi_door.close_device() + + def open_cover(self, **kwargs): + """Open the cover.""" + _LOGGER.debug("Opening garage door") + self.ryobi_door.open_device() + + def update(self): + """Update status from the door.""" + _LOGGER.debug("Updating RyobiGDO status") + self.ryobi_door.update() + self._door_state = self.ryobi_door.get_door_status() diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 0009986d45f..a2f90e49e3a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -19,8 +19,14 @@ "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", "title": "Link with deCONZ" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index bbab4029d7e..850645225d0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -19,8 +19,8 @@ from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts from .const import ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) + CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==38'] @@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry): def async_add_remote(sensors): """Setup remote from deCONZ.""" from pydeconz.sensor import SWITCH as DECONZ_REMOTE + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_REMOTE: + if sensor.type in DECONZ_REMOTE and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index e900782ea65..cb7c3aad7fd 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client from homeassistant.util.json import load_json -from .const import CONFIG_FILE, DOMAIN +from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN + +CONF_BRIDGEID = 'bridgeid' @callback def configured_hosts(hass): """Return a set of the configured hosts.""" - return set(entry.data['host'] for entry + return set(entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)) @@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): self.deconz_config = {} async def async_step_init(self, user_input=None): - """Handle a deCONZ config flow start.""" + """Handle a deCONZ config flow start. + + Only allows one instance to be set up. + If only one bridge is found go to link step. + If more than one bridge is found let user choose bridge to link. + """ from pydeconz.utils import async_discovery if configured_hosts(self.hass): @@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): async def async_step_link(self, user_input=None): """Attempt to link with the deCONZ bridge.""" - from pydeconz.utils import async_get_api_key, async_get_bridgeid + from pydeconz.utils import async_get_api_key errors = {} if user_input is not None: @@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): api_key = await async_get_api_key(session, **self.deconz_config) if api_key: self.deconz_config[CONF_API_KEY] = api_key - if 'bridgeid' not in self.deconz_config: - self.deconz_config['bridgeid'] = await async_get_bridgeid( - session, **self.deconz_config) - return self.async_create_entry( - title='deCONZ-' + self.deconz_config['bridgeid'], - data=self.deconz_config - ) + return await self.async_step_options() errors['base'] = 'no_key' return self.async_show_form( @@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): errors=errors, ) + async def async_step_options(self, user_input=None): + """Extra options for deCONZ. + + CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors. + """ + from pydeconz.utils import async_get_bridgeid + + if user_input is not None: + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \ + user_input[CONF_ALLOW_CLIP_SENSOR] + + if CONF_BRIDGEID not in self.deconz_config: + session = aiohttp_client.async_get_clientsession(self.hass) + self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + session, **self.deconz_config) + + return self.async_create_entry( + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config + ) + + return self.async_show_form( + step_id='options', + data_schema=vol.Schema({ + vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool, + }), + ) + async def async_step_discovery(self, discovery_info): """Prepare configuration for a discovered deCONZ bridge. @@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - deconz_config['bridgeid'] = discovery_info.get('serial') + deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') config_file = await self.hass.async_add_job( load_json, self.hass.config.path(CONFIG_FILE)) @@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - from pydeconz.utils import async_get_bridgeid - if configured_hosts(self.hass): return self.async_abort(reason='one_instance_only') - elif CONF_API_KEY not in import_config: - self.deconz_config = import_config + + self.deconz_config = import_config + if CONF_API_KEY not in import_config: return await self.async_step_link() - if 'bridgeid' not in import_config: - session = aiohttp_client.async_get_clientsession(self.hass) - import_config['bridgeid'] = await async_get_bridgeid( - session, **import_config) + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True return self.async_create_entry( - title='deCONZ-' + import_config['bridgeid'], - data=import_config + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 48e5ea75d68..43f3c6441da 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' + +CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7ea68af01c1..cabe58694d2 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,6 @@ { "config": { - "title": "deCONZ", + "title": "deCONZ Zigbee gateway", "step": { "init": { "title": "Define deCONZ gateway", @@ -12,6 +12,12 @@ "link": { "title": "Link with deCONZ", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data":{ + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, "error": { diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 3bf0cb0e126..5f06946fc44 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -4,19 +4,20 @@ Support for Google Maps location sharing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.google_maps/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID +from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify -REQUIREMENTS = ['locationsharinglib==2.0.2'] +REQUIREMENTS = ['locationsharinglib==2.0.7'] _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,7 @@ class GoogleMapsScanner(object): def _update_info(self, now=None): for person in self.service.get_all_people(): try: - dev_id = 'google_maps_{0}'.format(person.id) + dev_id = 'google_maps_{0}'.format(slugify(person.id)) except TypeError: _LOGGER.warning("No location(s) shared with this account") return diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index a4b826a009f..f479dea184b 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -15,14 +15,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import HomeAssistantError 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_USERNAME, CONF_PASSWORD, CONF_SSL) _LOGGER = logging.getLogger(__name__) +DEFAULT_SSL = False + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean }) @@ -44,7 +48,9 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - self.host = config[CONF_HOST] + host = config[CONF_HOST] + protocol = 'http' if not config[CONF_SSL] else 'https' + self.origin = '{}://{}'.format(protocol, host) self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] @@ -57,7 +63,7 @@ class LuciDeviceScanner(DeviceScanner): def refresh_token(self): """Get a new token.""" - self.token = _get_token(self.host, self.username, self.password) + self.token = _get_token(self.origin, self.username, self.password) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -67,9 +73,9 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: - url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) - result = _req_json_rpc(url, 'get_all', 'dhcp', - params={'auth': self.token}) + url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin) + result = _req_json_rpc( + url, 'get_all', 'dhcp', params={'auth': self.token}) if result: hosts = [x for x in result.values() if x['.type'] == 'host' and @@ -92,11 +98,11 @@ class LuciDeviceScanner(DeviceScanner): _LOGGER.info("Checking ARP") - url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) + url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin) try: - result = _req_json_rpc(url, 'net.arptable', - params={'auth': self.token}) + result = _req_json_rpc( + url, 'net.arptable', params={'auth': self.token}) except InvalidLuciTokenError: _LOGGER.info("Refreshing token") self.refresh_token() @@ -146,10 +152,10 @@ def _req_json_rpc(url, method, *args, **kwargs): raise InvalidLuciTokenError else: - _LOGGER.error('Invalid response from luci: %s', res) + _LOGGER.error("Invalid response from luci: %s", res) -def _get_token(host, username, password): - """Get authentication token for the given host+username+password.""" - url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host) +def _get_token(origin, username, password): + """Get authentication token for the given configuration.""" + url = '{}/cgi-bin/luci/rpc/auth'.format(origin) return _req_json_rpc(url, 'login', username, password) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index a24e82da106..69447b81cd4 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -83,6 +83,7 @@ SERVICE_HANDLERS = { 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), + 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 892c0b9972a..e86e7348d58 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.6'] +REQUIREMENTS = ['lakeside==0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5ebf6e8762f..5892e9136d8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180531.0'] +REQUIREMENTS = ['home-assistant-frontend==20180608.0b0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index aa24cc61af3..0fbb2a57ca9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -28,6 +28,15 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'hassio' DEPENDENCIES = ['http'] +CONF_FRONTEND_REPO = 'development_repo' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + }), +}, extra=vol.ALLOW_EXTRA) + + DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) @@ -142,7 +151,13 @@ def async_setup(hass, config): try: host = os.environ['HASSIO'] except KeyError: - _LOGGER.error("No Hass.io supervisor detect") + _LOGGER.error("Missing HASSIO environment variable.") + return False + + try: + os.environ['HASSIO_TOKEN'] + except KeyError: + _LOGGER.error("Missing HASSIO_TOKEN environment variable.") return False websession = hass.helpers.aiohttp_client.async_get_clientsession() @@ -152,11 +167,18 @@ def async_setup(hass, config): _LOGGER.error("Not connected with Hass.io") return False + # This overrides the normal API call that would be forwarded + development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) + if development_repo is not None: + hass.http.register_static_path( + '/api/hassio/app-es5', + os.path.join(development_repo, 'hassio/build-es5'), False) + hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:home-assistant') + 'hassio', 'Hass.io', 'hass:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index c27e394ce28..7ee1c70487f 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -274,7 +274,7 @@ async def async_setup(hass, config): hass.http.register_view(HistoryPeriodView(filters, use_include_order)) await hass.components.frontend.async_register_built_in_panel( - 'history', 'history', 'mdi:poll-box') + 'history', 'history', 'hass:poll-box') return True diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 202f9694689..34372b8b6a8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -9,12 +9,11 @@ from zlib import adler32 import voluptuous as vol -from homeassistant.components.cover import ( - SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) +import homeassistant.components.cover as cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -22,15 +21,16 @@ from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, - DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START) -from .util import show_setup_message, validate_entity_config + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, + DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) +from .util import ( + show_setup_message, validate_entity_config, validate_media_player_features) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.1.0'] +REQUIREMENTS = ['HAP-python==2.2.2'] # #### Driver Status #### STATUS_READY = 0 @@ -38,6 +38,8 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 +SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', + TYPE_SWITCH: 'Switch'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ @@ -84,7 +86,7 @@ async def async_setup(hass, config): return True -def get_accessory(hass, state, aid, config): +def get_accessory(hass, driver, state, aid, config): """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' @@ -109,11 +111,11 @@ def get_accessory(hass, state, aid, config): device_class = state.attributes.get(ATTR_DEVICE_CLASS) if device_class == 'garage' and \ - features & (SUPPORT_OPEN | SUPPORT_CLOSE): + features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'GarageDoorOpener' - elif features & SUPPORT_SET_POSITION: + elif features & cover.SUPPORT_SET_POSITION: a_type = 'WindowCovering' - elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): + elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' elif state.domain == 'fan': @@ -125,6 +127,12 @@ def get_accessory(hass, state, aid, config): elif state.domain == 'lock': a_type = 'Lock' + elif state.domain == 'media_player': + feature_list = config.get(CONF_FEATURE_LIST) + if feature_list and \ + validate_media_player_features(state, feature_list): + a_type = 'MediaPlayer' + elif state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -143,14 +151,18 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): + elif state.domain == 'switch': + switch_type = config.get(CONF_TYPE, TYPE_SWITCH) + a_type = SWITCH_TYPES[switch_type] + + elif state.domain in ('automation', 'input_boolean', 'remote', 'script'): a_type = 'Switch' if a_type is None: return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, name, state.entity_id, aid, config) + return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) def generate_aid(entity_id): @@ -185,9 +197,9 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) - self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.hass, self.bridge, port=self._port, - address=ip_addr, persist_file=path) + self.driver = HomeDriver(self.hass, address=ip_addr, + port=self._port, persist_file=path) + self.bridge = HomeBridge(self.hass, self.driver) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -195,7 +207,7 @@ class HomeKit(): return aid = generate_aid(state.entity_id) conf = self._config.pop(state.entity_id, {}) - acc = get_accessory(self.hass, state, aid, conf) + acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: self.bridge.add_accessory(acc) @@ -208,12 +220,12 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 type_covers, type_fans, type_lights, type_locks, - type_security_systems, type_sensors, type_switches, - type_thermostats) + type_media_players, type_security_systems, type_sensors, + type_switches, type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_driver(self.driver) + self.driver.add_accessory(self.bridge) if not self.driver.state.paired: show_setup_message(self.hass, self.driver.state.pincode) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ded4526b008..1b0d5ce1be4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,6 +1,6 @@ """Extend the basic Accessory and Bridge functions.""" from datetime import timedelta -from functools import wraps +from functools import partial, wraps from inspect import getmodule import logging @@ -27,35 +27,25 @@ _LOGGER = logging.getLogger(__name__) def debounce(func): """Decorator function. Debounce callbacks form HomeKit.""" @ha_callback - def call_later_listener(*args): + def call_later_listener(self, *args): """Callback listener called from call_later.""" - # pylint: disable=unsubscriptable-object - nonlocal lastargs, remove_listener - hass = lastargs['hass'] - hass.async_add_job(func, *lastargs['args']) - lastargs = remove_listener = None + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + self.hass.async_add_job(func, self, *debounce_params[1:]) @wraps(func) - def wrapper(*args): - """Wrapper starts async timer. - - The accessory must have 'self.hass' and 'self.entity_id' as attributes. - """ - # pylint: disable=not-callable - hass = args[0].hass - nonlocal lastargs, remove_listener - if remove_listener: - remove_listener() - lastargs = remove_listener = None - lastargs = {'hass': hass, 'args': [*args]} + def wrapper(self, *args): + """Wrapper starts async timer.""" + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + debounce_params[0]() # remove listener remove_listener = track_point_in_utc_time( - hass, call_later_listener, + self.hass, partial(call_later_listener, self), dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) - logger.debug('%s: Start %s timeout', args[0].entity_id, + self.debounce[func.__name__] = (remove_listener, *args) + logger.debug('%s: Start %s timeout', self.entity_id, func.__name__.replace('set_', '')) - remove_listener = None - lastargs = None name = getmodule(func).__name__ logger = logging.getLogger(name) return wrapper @@ -64,10 +54,10 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, config, + def __init__(self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER): """Initialize a Accessory object.""" - super().__init__(name, aid=aid) + super().__init__(driver, name, aid=aid) model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, @@ -76,11 +66,15 @@ class HomeAccessory(Accessory): self.config = config self.entity_id = entity_id self.hass = hass + self.debounce = {} - def run(self): - """Method called by accessory after driver is started.""" + async def run(self): + """Method called by accessory after driver is started. + + Run inside the HAP-python event loop. + """ state = self.hass.states.get(self.entity_id) - self.update_state_callback(new_state=state) + self.hass.add_job(self.update_state_callback, None, None, state) async_track_state_change( self.hass, self.entity_id, self.update_state_callback) @@ -104,9 +98,9 @@ class HomeAccessory(Accessory): class HomeBridge(Bridge): """Adapter class for Bridge.""" - def __init__(self, hass, name=BRIDGE_NAME): + def __init__(self, hass, driver, name=BRIDGE_NAME): """Initialize a Bridge object.""" - super().__init__(name) + super().__init__(driver, name) self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) @@ -120,17 +114,17 @@ class HomeBridge(Bridge): class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, *args, **kwargs): + def __init__(self, hass, **kwargs): """Initialize a AccessoryDriver object.""" - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.hass = hass def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" - value = super().pair(client_uuid, client_public) - if value: + success = super().pair(client_uuid, client_public) + if success: dismiss_setup_message(self.hass) - return value + return success def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 21cad2d9cf7..dec6353850e 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -8,12 +8,20 @@ HOMEKIT_NOTIFY_ID = 4663548 # #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' +CONF_FEATURE = 'feature' +CONF_FEATURE_LIST = 'feature_list' CONF_FILTER = 'filter' # #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 +# #### Features #### +FEATURE_ON_OFF = 'on_off' +FEATURE_PLAY_PAUSE = 'play_pause' +FEATURE_PLAY_STOP = 'play_stop' +FEATURE_TOGGLE_MUTE = 'toggle_mute' + # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' @@ -23,6 +31,10 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' +# #### Switch Types #### +TYPE_OUTLET = 'outlet' +TYPE_SWITCH = 'switch' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' @@ -38,6 +50,7 @@ SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' +SERV_OUTLET = 'Outlet' SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' @@ -76,6 +89,7 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' +CHAR_OUTLET_IN_USE = 'OutletInUse' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py new file mode 100644 index 00000000000..ec41b9fd618 --- /dev/null +++ b/homeassistant/components/homekit/type_media_players.py @@ -0,0 +1,142 @@ +"""Class to hold all media player accessories.""" +import logging + +from pyhap.const import CATEGORY_SWITCH + +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, + STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH) + +_LOGGER = logging.getLogger(__name__) + +MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power', + FEATURE_PLAY_PAUSE: 'Play/Pause', + FEATURE_PLAY_STOP: 'Play/Stop', + FEATURE_TOGGLE_MUTE: 'Mute'} + + +@TYPES.register('MediaPlayer') +class MediaPlayer(HomeAccessory): + """Generate a Media Player accessory.""" + + def __init__(self, *args): + """Initialize a Switch accessory object.""" + super().__init__(*args, category=CATEGORY_SWITCH) + self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False, + FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False} + self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None} + feature_list = self.config[CONF_FEATURE_LIST] + + if FEATURE_ON_OFF in feature_list: + name = self.generate_service_name(FEATURE_ON_OFF) + serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_on_off.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char( + CHAR_ON, value=False, setter_callback=self.set_on_off) + + if FEATURE_PLAY_PAUSE in feature_list: + name = self.generate_service_name(FEATURE_PLAY_PAUSE) + serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_pause.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_pause) + + if FEATURE_PLAY_STOP in feature_list: + name = self.generate_service_name(FEATURE_PLAY_STOP) + serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_stop.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_stop) + + if FEATURE_TOGGLE_MUTE in feature_list: + name = self.generate_service_name(FEATURE_TOGGLE_MUTE) + serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_toggle_mute.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( + CHAR_ON, value=False, setter_callback=self.set_toggle_mute) + + def generate_service_name(self, mode): + """Generate name for individual service.""" + return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode]) + + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "on_off" to %s', + self.entity_id, value) + self._flag[FEATURE_ON_OFF] = True + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_pause(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_pause" to %s', + self.entity_id, value) + self._flag[FEATURE_PLAY_PAUSE] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_stop(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_stop" to %s', + self.entity_id, value) + self._flag[FEATURE_PLAY_STOP] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_toggle_mute(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', + self.entity_id, value) + self._flag[FEATURE_TOGGLE_MUTE] = True + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_MEDIA_VOLUME_MUTED: value} + self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = new_state.state + + if self.chars[FEATURE_ON_OFF]: + hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') + if not self._flag[FEATURE_ON_OFF]: + _LOGGER.debug('%s: Set current state for "on_off" to %s', + self.entity_id, hk_state) + self.chars[FEATURE_ON_OFF].set_value(hk_state) + self._flag[FEATURE_ON_OFF] = False + + if self.chars[FEATURE_PLAY_PAUSE]: + hk_state = current_state == STATE_PLAYING + if not self._flag[FEATURE_PLAY_PAUSE]: + _LOGGER.debug('%s: Set current state for "play_pause" to %s', + self.entity_id, hk_state) + self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) + self._flag[FEATURE_PLAY_PAUSE] = False + + if self.chars[FEATURE_PLAY_STOP]: + hk_state = current_state == STATE_PLAYING + if not self._flag[FEATURE_PLAY_STOP]: + _LOGGER.debug('%s: Set current state for "play_stop" to %s', + self.entity_id, hk_state) + self.chars[FEATURE_PLAY_STOP].set_value(hk_state) + self._flag[FEATURE_PLAY_STOP] = False + + if self.chars[FEATURE_TOGGLE_MUTE]: + current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) + if not self._flag[FEATURE_TOGGLE_MUTE]: + _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', + self.entity_id, current_state) + self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) + self._flag[FEATURE_TOGGLE_MUTE] = False diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 5754266587c..c8bf8c7ad7c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,25 +1,60 @@ """Class to hold all switch accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH +from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH +from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import SERV_SWITCH, CHAR_ON +from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH _LOGGER = logging.getLogger(__name__) +@TYPES.register('Outlet') +class Outlet(HomeAccessory): + """Generate an Outlet accessory.""" + + def __init__(self, *args): + """Initialize an Outlet accessory object.""" + super().__init__(*args, category=CATEGORY_OUTLET) + self.flag_target_state = False + + serv_outlet = self.add_preload_service(SERV_OUTLET) + self.char_on = serv_outlet.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) + self.char_outlet_in_use = serv_outlet.configure_char( + CHAR_OUTLET_IN_USE, value=True) + + def set_state(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state to %s', + self.entity_id, value) + self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(SWITCH, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = (new_state.state == STATE_ON) + if not self.flag_target_state: + _LOGGER.debug('%s: Set current state to %s', + self.entity_id, current_state) + self.char_on.set_value(current_state) + self.flag_target_state = False + + @TYPES.register('Switch') class Switch(HomeAccessory): """Generate a Switch accessory.""" def __init__(self, *args): - """Initialize a Switch accessory object to represent a remote.""" + """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d6555d5056d..73a29990fba 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,15 +4,16 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import debounce, HomeAccessory @@ -20,7 +21,7 @@ from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) + CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -42,17 +43,18 @@ class Thermostat(HomeAccessory): def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) - self._unit = TEMP_CELSIUS + self._unit = self.hass.config.units.temperature_unit self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False + min_temp, max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + .attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_ON_OFF: self.support_power_state = True if features & SUPPORT_TEMP_RANGE: @@ -73,6 +75,8 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_TEMPERATURE, value=21.0) self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=21.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature) # Display units characteristic @@ -85,12 +89,30 @@ class Thermostat(HomeAccessory): if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: self.char_cooling_thresh_temp = serv_thermostat.configure_char( CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold) + def get_temperature_range(self): + """Return min and max temperature range.""" + max_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_TEMP) + max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + else DEFAULT_MAX_TEMP + + min_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_TEMP) + min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + else DEFAULT_MIN_TEMP + + return min_temp, max_temp + def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -147,9 +169,6 @@ class Thermostat(HomeAccessory): def update_state(self, new_state): """Update security state after state changed.""" - self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS) - # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 447257f9e8f..6a43a0c6228 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,41 +3,108 @@ import logging import voluptuous as vol +import homeassistant.components.media_player as media_player from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util -from .const import HOMEKIT_NOTIFY_ID +from .const import ( + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, + TYPE_SWITCH) _LOGGER = logging.getLogger(__name__) +BASIC_INFO_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, +}) + +FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list, +}) + + +CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string), +}) + +MEDIA_PLAYER_SCHEMA = vol.Schema({ + vol.Required(CONF_FEATURE): vol.All( + cv.string, vol.In((FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), +}) + +SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( + cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), +}) + + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" entities = {} for entity_id, config in values.items(): entity = cv.entity_id(entity_id) - params = {} - if not isinstance(config, dict): - raise vol.Invalid('The configuration for "{}" must be ' - ' a dictionary.'.format(entity)) - - for key in (CONF_NAME, ): - value = config.get(key, -1) - if value != -1: - params[key] = cv.string(value) - domain, _ = split_entity_id(entity) - if domain in ('alarm_control_panel', 'lock'): - code = config.get(ATTR_CODE) - params[ATTR_CODE] = cv.string(code) if code else None + if not isinstance(config, dict): + raise vol.Invalid('The configuration for {} must be ' + ' a dictionary.'.format(entity)) - entities[entity] = params + if domain in ('alarm_control_panel', 'lock'): + config = CODE_SCHEMA(config) + + elif domain == media_player.DOMAIN: + config = FEATURE_SCHEMA(config) + feature_list = {} + for feature in config[CONF_FEATURE_LIST]: + params = MEDIA_PLAYER_SCHEMA(feature) + key = params.pop(CONF_FEATURE) + if key in feature_list: + raise vol.Invalid('A feature can be added only once for {}' + .format(entity)) + feature_list[key] = params + config[CONF_FEATURE_LIST] = feature_list + + elif domain == 'switch': + config = SWITCH_TYPE_SCHEMA(config) + + else: + config = BASIC_INFO_SCHEMA(config) + + entities[entity] = config return entities +def validate_media_player_features(state, feature_list): + """Validate features for media players.""" + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + supported_modes = [] + if features & (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF): + supported_modes.append(FEATURE_ON_OFF) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE): + supported_modes.append(FEATURE_PLAY_PAUSE) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_STOP): + supported_modes.append(FEATURE_PLAY_STOP) + if features & media_player.SUPPORT_VOLUME_MUTE: + supported_modes.append(FEATURE_TOGGLE_MUTE) + + error_list = [] + for feature in feature_list: + if feature not in supported_modes: + error_list.append(feature) + + if error_list: + _LOGGER.error("%s does not support features: %s", + state.entity_id, error_list) + return False + return True + + def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" pin = pincode.decode() diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index e0f0fafe5b5..29303b551e2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.42'] +REQUIREMENTS = ['pyhomematic==0.1.43'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index d85d867d8f8..859841dfca6 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -17,7 +17,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.core import callback -REQUIREMENTS = ['homematicip==0.9.2.4'] +REQUIREMENTS = ['homematicip==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,8 @@ COMPONENTS = [ 'sensor', 'binary_sensor', 'switch', - 'light' + 'light', + 'climate', ] CONF_NAME = 'name' diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py new file mode 100644 index 00000000000..a60e3d5b8fc --- /dev/null +++ b/homeassistant/components/hydrawise.py @@ -0,0 +1,153 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/hydrawise/ +""" +import asyncio +from datetime import timedelta +import logging + +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL) +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['hydrawiser==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] + +CONF_ATTRIBUTION = "Data provided by hydrawise.com" +CONF_WATERING_TIME = 'watering_minutes' + +NOTIFICATION_ID = 'hydrawise_notification' +NOTIFICATION_TITLE = 'Hydrawise Setup' + +DATA_HYDRAWISE = 'hydrawise' +DOMAIN = 'hydrawise' +DEFAULT_WATERING_TIME = 15 + +DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX', + 'UNIT_OF_MEASURE_INDEX'] +DEVICE_MAP = { + 'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''], + 'is_watering': ['Watering', '', 'moisture', ''], + 'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''], + 'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''], + 'status': ['Status', '', 'connectivity', ''], + 'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'], + 'rain_sensor': ['Rain Sensor', '', 'moisture', ''] +} + +BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor'] + +SENSORS = ['next_cycle', 'watering_time'] + +SWITCHES = ['auto_watering', 'manual_watering'] + +SCAN_INTERVAL = timedelta(seconds=30) + +SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Hunter Hydrawise component.""" + conf = config[DOMAIN] + access_token = conf[CONF_ACCESS_TOKEN] + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from hydrawiser.core import Hydrawiser + + hydrawise = Hydrawiser(user_token=access_token) + hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error( + "Unable to connect to Hydrawise cloud service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + def hub_refresh(event_time): + """Call Hydrawise hub to refresh information.""" + _LOGGER.debug("Updating Hydrawise Hub component") + hass.data[DATA_HYDRAWISE].data.update_controller_info() + dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE) + + # Call the Hydrawise API to refresh updates + track_time_interval(hass, hub_refresh, scan_interval) + + return True + + +class HydrawiseHub(object): + """Representation of a base Hydrawise device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class HydrawiseEntity(Entity): + """Entity class for Hydrawise devices.""" + + def __init__(self, data, sensor_type): + """Initialize the Hydrawise entity.""" + self.data = data + self._sensor_type = sensor_type + self._name = "{0} {1}".format( + self.data['name'], + DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('KEY_INDEX')]) + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'identifier': self.data.get('relay'), + } diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index d737c555873..af45bd3d4f9 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -50,10 +50,7 @@ def setup(hass, config): """Set up the keyboard_remote.""" config = config.get(DOMAIN) - keyboard_remote = KeyboardRemote( - hass, - config - ) + keyboard_remote = KeyboardRemote(hass, config) def _start_keyboard_remote(_event): keyboard_remote.run() @@ -61,14 +58,8 @@ def setup(hass, config): def _stop_keyboard_remote(_event): keyboard_remote.stop() - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, - _start_keyboard_remote - ) - hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, - _stop_keyboard_remote - ) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) return True @@ -93,10 +84,8 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug("Keyboard connected, %s", self.device_id) else: _LOGGER.debug( - 'Keyboard not connected, %s.\n\ - Check /dev/input/event* permissions.', - self.device_id - ) + "Keyboard not connected, %s. " + "Check /dev/input/event* permissions", self.device_id) id_folder = '/dev/input/by-id/' @@ -105,12 +94,9 @@ class KeyboardRemoteThread(threading.Thread): device_names = [InputDevice(file_name).name for file_name in list_devices()] _LOGGER.debug( - 'Possible device names are:\n %s.\n \ - Possible device descriptors are %s:\n %s', - device_names, - id_folder, - os.listdir(id_folder) - ) + "Possible device names are: %s. " + "Possible device descriptors are %s: %s", + device_names, id_folder, os.listdir(id_folder)) threading.Thread.__init__(self) self.stopped = threading.Event() @@ -149,9 +135,7 @@ class KeyboardRemoteThread(threading.Thread): self.dev = self._get_keyboard_device() if self.dev is not None: self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_CONNECTED) _LOGGER.debug("Keyboard re-connected, %s", self.device_id) else: continue @@ -160,9 +144,7 @@ class KeyboardRemoteThread(threading.Thread): event = self.dev.read_one() except IOError: # Keyboard Disconnected self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_DISCONNECTED) _LOGGER.debug("Keyboard disconnected, %s", self.device_id) continue @@ -174,7 +156,11 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug(categorize(event)) self.hass.bus.fire( KEYBOARD_REMOTE_COMMAND_RECEIVED, - {KEY_CODE: event.code} + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } ) @@ -191,9 +177,8 @@ class KeyboardRemote(object): if device_descriptor is not None\ or device_name is not None: - thread = KeyboardRemoteThread(hass, device_name, - device_descriptor, - key_value) + thread = KeyboardRemoteThread( + hass, device_name, device_descriptor, key_value) self.threads.append(thread) def run(self): diff --git a/homeassistant/components/light/lw12wifi.py b/homeassistant/components/light/lw12wifi.py new file mode 100644 index 00000000000..f81d8368f98 --- /dev/null +++ b/homeassistant/components/light/lw12wifi.py @@ -0,0 +1,158 @@ +""" +Support for Lagute LW-12 WiFi LED Controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.lw12wifi/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_TRANSITION +) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT +) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + + +REQUIREMENTS = ['lw12==0.9.2'] + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_NAME = 'LW-12 FC' +DEFAULT_PORT = 5000 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup LW-12 WiFi LED Controller platform.""" + import lw12 + + # Assign configuration variables. + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + # Add devices + lw12_light = lw12.LW12Controller(host, port) + add_devices([LW12WiFi(name, lw12_light)]) + + +class LW12WiFi(Light): + """LW-12 WiFi LED Controller.""" + + def __init__(self, name, lw12_light): + """Initialisation of LW-12 WiFi LED Controller. + + Args: + name: Friendly name for this platform to use. + lw12_light: Instance of the LW12 controller. + """ + self._light = lw12_light + self._name = name + self._state = None + self._effect = None + self._rgb_color = [255, 255, 255] + self._brightness = 255 + # Setup feature list + self._supported_features = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT \ + | SUPPORT_COLOR | SUPPORT_TRANSITION + + @property + def name(self): + """Return the display name of the controlled light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Read back the hue-saturation of the light.""" + return color_util.color_RGB_to_hs(*self._rgb_color) + + @property + def effect(self): + """Return current light effect.""" + if self._effect is None: + return None + return self._effect.replace('_', ' ').title() + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Return a list of supported features.""" + return self._supported_features + + @property + def effect_list(self): + """Return a list of available effects. + + Use the Enum element name for display. + """ + import lw12 + return [effect.name.replace('_', ' ').title() + for effect in lw12.LW12_EFFECT] + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return True + + @property + def shoud_poll(self) -> bool: + """Return False to not poll the state of this entity.""" + return False + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import lw12 + self._light.light_on() + if ATTR_HS_COLOR in kwargs: + self._rgb_color = color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR]) + self._light.set_color(*self._rgb_color) + self._effect = None + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = int(self._brightness / 255 * 100) + self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS, + brightness) + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT].replace(' ', '_').upper() + # Check if a known and supported effect was selected. + if self._effect in [eff.name for eff in lw12.LW12_EFFECT]: + # Selected effect is supported and will be applied. + self._light.set_effect(lw12.LW12_EFFECT[self._effect]) + else: + # Unknown effect was set, recover by disabling the effect + # mode and log an error. + _LOGGER.error("Unknown effect selected: %s", self._effect) + self._effect = None + if ATTR_TRANSITION in kwargs: + transition_speed = int(kwargs[ATTR_TRANSITION]) + self._light.set_light_option(lw12.LW12_LIGHT.FLASH, + transition_speed) + self._state = True + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.light_off() + self._state = False diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index c26766d8deb..6a0d3c36e9f 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin +from homeassistant.util.json import load_json, save_json REQUIREMENTS = ['nanoleaf==0.4.1'] @@ -24,6 +25,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Aurora' +DATA_NANOLEAF_AURORA = 'nanoleaf_aurora' + +CONFIG_FILE = '.nanoleaf_aurora.conf' + ICON = 'mdi:triangle-outline' SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | @@ -39,31 +44,59 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Nanoleaf Aurora device.""" import nanoleaf - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + import nanoleaf.setup + if DATA_NANOLEAF_AURORA not in hass.data: + hass.data[DATA_NANOLEAF_AURORA] = dict() + + token = '' + if discovery_info is not None: + host = discovery_info['host'] + name = discovery_info['hostname'] + # if device already exists via config, skip discovery setup + if host in hass.data[DATA_NANOLEAF_AURORA]: + return + _LOGGER.info("Discovered a new Aurora: %s", discovery_info) + conf = load_json(hass.config.path(CONFIG_FILE)) + if conf.get(host, {}).get('token'): + token = conf[host]['token'] + else: + host = config[CONF_HOST] + name = config[CONF_NAME] + token = config[CONF_TOKEN] + + if not token: + token = nanoleaf.setup.generate_auth_token(host) + if not token: + _LOGGER.error("Could not generate the auth token, did you press " + "and hold the power button on %s" + "for 5-7 seconds?", name) + return + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {'token': token} + save_json(hass.config.path(CONFIG_FILE), conf) + aurora_light = nanoleaf.Aurora(host, token) - aurora_light.hass_name = name if aurora_light.on is None: _LOGGER.error( "Could not connect to Nanoleaf Aurora: %s on %s", name, host) return - add_devices([AuroraLight(aurora_light)], True) + hass.data[DATA_NANOLEAF_AURORA][host] = aurora_light + add_devices([AuroraLight(aurora_light, name)], True) class AuroraLight(Light): """Representation of a Nanoleaf Aurora.""" - def __init__(self, light): + def __init__(self, light, name): """Initialize an Aurora light.""" self._brightness = None self._color_temp = None self._effect = None self._effects_list = None self._light = light - self._name = light.hass_name + self._name = name self._hs_color = None self._state = None diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 2c44620caca..939d0fe6988 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -27,8 +27,10 @@ REQUIREMENTS = ['lightify==1.0.6.1'] _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_LIGHTIFY_NODES = 'allow_lightify_nodes' CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' +DEFAULT_ALLOW_LIGHTIFY_NODES = True DEFAULT_ALLOW_LIGHTIFY_GROUPS = True MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -40,6 +42,8 @@ SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ALLOW_LIGHTIFY_NODES, + default=DEFAULT_ALLOW_LIGHTIFY_NODES): cv.boolean, vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean, }) @@ -50,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import lightify host = config.get(CONF_HOST) + add_nodes = config.get(CONF_ALLOW_LIGHTIFY_NODES) add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) try: @@ -60,10 +65,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.exception(msg) return - setup_bridge(bridge, add_devices, add_groups) + setup_bridge(bridge, add_devices, add_nodes, add_groups) -def setup_bridge(bridge, add_devices, add_groups): +def setup_bridge(bridge, add_devices, add_nodes, add_groups): """Set up the Lightify bridge.""" lights = {} @@ -80,14 +85,15 @@ def setup_bridge(bridge, add_devices, add_groups): new_lights = [] - for (light_id, light) in bridge.lights().items(): - if light_id not in lights: - osram_light = OsramLightifyLight( - light_id, light, update_lights) - lights[light_id] = osram_light - new_lights.append(osram_light) - else: - lights[light_id].light = light + if add_nodes: + for (light_id, light) in bridge.lights().items(): + if light_id not in lights: + osram_light = OsramLightifyLight( + light_id, light, update_lights) + lights[light_id] = osram_light + new_lights.append(osram_light) + else: + lights[light_id].light = light if add_groups: for (group_name, group) in bridge.groups().items(): diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index b44bf820b23..bd01a513e0b 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -172,7 +172,8 @@ class Light(zha.Entity, light.Light): result = await zha.safe_read(self._endpoint.light_color, ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - xy_color = (result['current_x'], result['current_y']) + xy_color = (round(result['current_x']/65535, 3), + round(result['current_y']/65535, 3)) self._hs_color = color_util.color_xy_to_hs(*xy_color) @property diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode.py index 9e87c002482..962e30774b8 100644 --- a/homeassistant/components/linode.py +++ b/homeassistant/components/linode.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['linode-api==4.1.4b2'] +REQUIREMENTS = ['linode-api==4.1.9b1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lock/xiaomi_aqara.py b/homeassistant/components/lock/xiaomi_aqara.py new file mode 100644 index 00000000000..9b084a2bc55 --- /dev/null +++ b/homeassistant/components/lock/xiaomi_aqara.py @@ -0,0 +1,92 @@ +""" +Support for Xiaomi Aqara Lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.xiaomi_aqara/ +""" +import logging +from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, + XiaomiDevice) +from homeassistant.components.lock import LockDevice +from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.helpers.event import async_call_later +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +FINGER_KEY = 'fing_verified' +PASSWORD_KEY = 'psw_verified' +CARD_KEY = 'card_verified' +VERIFIED_WRONG_KEY = 'verified_wrong' + +ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times' + +UNLOCK_MAINTAIN_TIME = 5 + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Perform the setup for Xiaomi devices.""" + devices = [] + + for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values(): + for device in gateway.devices['lock']: + model = device['model'] + if model == 'lock.aq1': + devices.append(XiaomiAqaraLock(device, 'Lock', gateway)) + async_add_devices(devices) + + +class XiaomiAqaraLock(LockDevice, XiaomiDevice): + """Representation of a XiaomiAqaraLock.""" + + def __init__(self, device, name, xiaomi_hub): + """Initialize the XiaomiAqaraLock.""" + self._changed_by = 0 + self._verified_wrong_times = 0 + + super().__init__(device, name, xiaomi_hub) + + @property + def is_locked(self) -> bool: + """Return true if lock is locked.""" + if self._state is not None: + return self._state == STATE_LOCKED + + @property + def changed_by(self) -> int: + """Last change triggered by.""" + return self._changed_by + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + attributes = { + ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times, + } + return attributes + + @callback + def clear_unlock_state(self, _): + """Clear unlock state automatically.""" + self._state = STATE_LOCKED + self.async_schedule_update_ha_state() + + def parse_data(self, data, raw_data): + """Parse data sent by gateway.""" + value = data.get(VERIFIED_WRONG_KEY) + if value is not None: + self._verified_wrong_times = int(value) + return True + + for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): + value = data.get(key) + if value is not None: + self._changed_by = int(value) + self._verified_wrong_times = 0 + self._state = STATE_UNLOCKED + async_call_later(self.hass, UNLOCK_MAINTAIN_TIME, + self.clear_unlock_state) + return True + + return False diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1ea0b586d33..bcfae533abf 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -100,7 +100,7 @@ async def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) await hass.components.frontend.async_register_built_in_panel( - 'logbook', 'logbook', 'mdi:format-list-bulleted-type') + 'logbook', 'logbook', 'hass:format-list-bulleted-type') hass.services.async_register( DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 89cc296111b..75b90b084fc 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==2018.05.09'] +REQUIREMENTS = ['youtube_dl==2018.06.02'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index fe8fc46c24b..74d3c5a0785 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.6.1'] +REQUIREMENTS = ['denonavr==0.7.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 25d13e3017a..0adb02b6a65 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['directpy==0.2'] +REQUIREMENTS = ['directpy==0.5'] DEFAULT_DEVICE = '0' DEFAULT_NAME = 'DirecTV Receiver' diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 770d57b5b8e..68a9da55ae4 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -294,6 +294,7 @@ class KodiDevice(MediaPlayerDevice): # Register notification listeners self._ws_server.Player.OnPause = self.async_on_speed_event self._ws_server.Player.OnPlay = self.async_on_speed_event + self._ws_server.Player.OnResume = self.async_on_speed_event self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event self._ws_server.Player.OnStop = self.async_on_stop self._ws_server.Application.OnVolumeChanged = \ @@ -541,8 +542,8 @@ class KodiDevice(MediaPlayerDevice): def media_title(self): """Title of current playing media.""" # find a string we can use as a title - return self._item.get( - 'title', self._item.get('label', self._item.get('file'))) + item = self._item + return item.get('title') or item.get('label') or item.get('file') @property def media_series_title(self): diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index d526fbb0387..01d63e0b6c8 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -13,20 +13,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.3'] +REQUIREMENTS = ['ha-philipsjs==0.0.4'] _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_SELECT_SOURCE SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY @@ -165,6 +167,10 @@ class PhilipsTV(MediaPlayerDevice): if not self._tv.on: self._state = STATE_OFF + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._tv.setVolume(volume) + def media_previous_track(self): """Send rewind command.""" self._tv.sendKey('Previous') @@ -189,12 +195,10 @@ class PhilipsTV(MediaPlayerDevice): self._volume = self._tv.volume self._muted = self._tv.muted if self._tv.source_id: - src = self._tv.sources.get(self._tv.source_id, None) - if src: - self._source = src.get('name', None) + self._source = self._tv.getSourceName(self._tv.source_id) if self._tv.sources and not self._source_list: - for srcid in sorted(self._tv.sources): - srcname = self._tv.sources.get(srcid, dict()).get('name', None) + for srcid in self._tv.sources: + srcname = self._tv.getSourceName(srcid) self._source_list.append(srcname) self._source_mapping[srcname] = srcid if self._tv.on: diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index e7d2ba90438..16a0b80d1fd 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -4,18 +4,22 @@ Support for Nest devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/nest/ """ +from concurrent.futures import ThreadPoolExecutor import logging import socket import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, - CONF_MONITORED_CONDITIONS) + CONF_MONITORED_CONDITIONS, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send, \ + async_dispatcher_connect +from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-nest==3.7.0'] +REQUIREMENTS = ['python-nest==4.0.1'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -24,6 +28,8 @@ DOMAIN = 'nest' DATA_NEST = 'nest' +SIGNAL_NEST_UPDATE = 'nest_update' + NEST_CONFIG_FILE = 'nest.conf' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' @@ -51,23 +57,44 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def request_configuration(nest, hass, config): +async def async_nest_update_event_broker(hass, nest): + """ + Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. + + nest.update_event.wait will block the thread in most of time, + so specific an executor to save default thread pool. + """ + _LOGGER.debug("listening nest.update_event") + with ThreadPoolExecutor(max_workers=1) as executor: + while True: + await hass.loop.run_in_executor(executor, nest.update_event.wait) + if hass.is_running: + nest.update_event.clear() + _LOGGER.debug("dispatching nest data update") + async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) + else: + return + + +async def async_request_configuration(nest, hass, config): """Request configuration steps from the user.""" configurator = hass.components.configurator if 'nest' in _CONFIGURING: _LOGGER.debug("configurator failed") - configurator.notify_errors( + configurator.async_notify_errors( _CONFIGURING['nest'], "Failed to configure, please try again.") return - def nest_configuration_callback(data): + async def async_nest_config_callback(data): """Run when the configuration callback is called.""" _LOGGER.debug("configurator callback") pin = data.get('pin') - setup_nest(hass, nest, config, pin=pin) + if await async_setup_nest(hass, nest, config, pin=pin): + # start nest update event listener as we missed startup hook + hass.async_add_job(async_nest_update_event_broker, hass, nest) - _CONFIGURING['nest'] = configurator.request_config( - "Nest", nest_configuration_callback, + _CONFIGURING['nest'] = configurator.async_request_config( + "Nest", async_nest_config_callback, description=('To configure Nest, click Request Authorization below, ' 'log into your Nest account, ' 'and then enter the resulting PIN'), @@ -78,60 +105,47 @@ def request_configuration(nest, hass, config): ) -def setup_nest(hass, nest, config, pin=None): +async def async_setup_nest(hass, nest, config, pin=None): """Set up the Nest devices.""" + from nest.nest import AuthorizationError, APIError if pin is not None: _LOGGER.debug("pin acquired, requesting access token") - nest.request_token(pin) + error_message = None + try: + nest.request_token(pin) + except AuthorizationError as auth_error: + error_message = "Nest authorization failed: {}".format(auth_error) + except APIError as api_error: + error_message = "Failed to call Nest API: {}".format(api_error) + + if error_message is not None: + _LOGGER.warning(error_message) + hass.components.configurator.async_notify_errors( + _CONFIGURING['nest'], error_message) + return False if nest.access_token is None: _LOGGER.debug("no access_token, requesting configuration") - request_configuration(nest, hass, config) - return + await async_request_configuration(nest, hass, config) + return False if 'nest' in _CONFIGURING: _LOGGER.debug("configuration done") configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('nest')) + configurator.async_request_done(_CONFIGURING.pop('nest')) _LOGGER.debug("proceeding with setup") conf = config[DOMAIN] hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - _LOGGER.debug("proceeding with discovery") - discovery.load_platform(hass, 'climate', DOMAIN, {}, config) - discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - - sensor_config = conf.get(CONF_SENSORS, {}) - discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - - binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - binary_sensor_config, config) - - _LOGGER.debug("setup done") - - return True - - -def setup(hass, config): - """Set up the Nest thermostat component.""" - import nest - - if 'nest' in _CONFIGURING: - return - - conf = config[DOMAIN] - client_id = conf[CONF_CLIENT_ID] - client_secret = conf[CONF_CLIENT_SECRET] - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - - access_token_cache_file = hass.config.path(filename) - - nest = nest.Nest( - access_token_cache_file=access_token_cache_file, - client_id=client_id, client_secret=client_secret) - setup_nest(hass, nest, config) + for component, discovered in [ + ('climate', {}), + ('camera', {}), + ('sensor', conf.get(CONF_SENSORS, {})), + ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]: + _LOGGER.debug("proceeding with discovery -- %s", component) + hass.async_add_job(discovery.async_load_platform, + hass, component, DOMAIN, discovered, config) def set_mode(service): """Set the home/away mode for a Nest structure.""" @@ -148,9 +162,47 @@ def setup(hass, config): _LOGGER.error("Invalid structure %s", service.data[ATTR_STRUCTURE]) - hass.services.register( + hass.services.async_register( DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + def start_up(event): + """Start Nest update event listener.""" + hass.async_add_job(async_nest_update_event_broker, hass, nest) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + + def shut_down(event): + """Stop Nest update event listener.""" + if nest: + nest.update_event.set() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + + _LOGGER.debug("async_setup_nest is done") + + return True + + +async def async_setup(hass, config): + """Set up Nest components.""" + from nest import Nest + + if 'nest' in _CONFIGURING: + return + + conf = config[DOMAIN] + client_id = conf[CONF_CLIENT_ID] + client_secret = conf[CONF_CLIENT_SECRET] + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + + access_token_cache_file = hass.config.path(filename) + + nest = Nest( + access_token_cache_file=access_token_cache_file, + client_id=client_id, client_secret=client_secret) + + await async_setup_nest(hass, nest, config) + return True @@ -168,6 +220,19 @@ class NestDevice(object): self.local_structure = conf[CONF_STRUCTURE] _LOGGER.debug("Structures to include: %s", self.local_structure) + def structures(self): + """Generate a list of structures.""" + try: + for structure in self.nest.structures: + if structure.name in self.local_structure: + yield structure + else: + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") + def thermostats(self): """Generate a list of thermostats and their location.""" try: @@ -188,10 +253,10 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.smoke_co_alarms: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") @@ -202,10 +267,61 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.cameras: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") + + +class NestSensorDevice(Entity): + """Representation of a Nest sensor.""" + + def __init__(self, structure, device, variable): + """Initialize the sensor.""" + self.structure = structure + self.variable = variable + + if device is not None: + # device specific + self.device = device + self._name = "{} {}".format(self.device.name_long, + self.variable.replace('_', ' ')) + else: + # structure only + self.device = structure + self._name = "{} {}".format(self.structure.name, + self.variable.replace('_', ' ')) + + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + def update(self): + """Do not use NestSensorDevice directly.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) diff --git a/homeassistant/components/notify/flock.py b/homeassistant/components/notify/flock.py new file mode 100644 index 00000000000..d26f629809f --- /dev/null +++ b/homeassistant/components/notify/flock.py @@ -0,0 +1,61 @@ +""" +Flock platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.flock/ +""" +import asyncio +import logging + +import async_timeout +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://api.flock.com/hooks/sendMessage/' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, +}) + + +async def get_service(hass, config, discovery_info=None): + """Get the Flock notification service.""" + access_token = config.get(CONF_ACCESS_TOKEN) + url = '{}{}'.format(_RESOURCE, access_token) + session = async_get_clientsession(hass) + + return FlockNotificationService(url, session, hass.loop) + + +class FlockNotificationService(BaseNotificationService): + """Implement the notification service for Flock.""" + + def __init__(self, url, session, loop): + """Initialize the Flock notification service.""" + self._loop = loop + self._url = url + self._session = session + + async def async_send_message(self, message, **kwargs): + """Send the message to the user.""" + payload = {'text': message} + + _LOGGER.debug("Attempting to call Flock at %s", self._url) + + try: + with async_timeout.timeout(10, loop=self._loop): + response = await self._session.post(self._url, json=payload) + result = await response.json() + + if response.status != 200 or 'error' in result: + _LOGGER.error( + "Flock service returned HTTP status %d, response %s", + response.status, result) + except asyncio.TimeoutError: + _LOGGER.error("Timeout accessing Flock at %s", self._url) diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py deleted file mode 100644 index e81dc457a81..00000000000 --- a/homeassistant/components/notify/nma.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -NMA (Notify My Android) notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.nma/ -""" -import logging -import xml.etree.ElementTree as ET - -import requests -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://www.notifymyandroid.com/publicapi/' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the NMA notification service.""" - parameters = { - 'apikey': config[CONF_API_KEY], - } - response = requests.get( - '{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.error("Wrong API key supplied: %s", tree[0].text) - return None - - return NmaNotificationService(config[CONF_API_KEY]) - - -class NmaNotificationService(BaseNotificationService): - """Implement the notification service for NMA.""" - - def __init__(self, api_key): - """Initialize the service.""" - self._api_key = api_key - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - data = { - 'apikey': self._api_key, - 'application': 'home-assistant', - 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - 'description': message, - 'priority': 0, - } - - response = requests.get( - '{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.exception( - "Unable to perform request. Error: %s", tree[0].text) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 9489e05cfa5..e38e7fcaa0f 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.0'] +REQUIREMENTS = ['TwitterAPI==2.5.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py index 473d44f3b55..4659578ae27 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom.py @@ -4,7 +4,6 @@ Register a custom front end panel. For more details about this component, please refer to the documentation at https://home-assistant.io/components/panel_custom/ """ -import asyncio import logging import os @@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon' CONF_URL_PATH = 'url_path' CONF_CONFIG = 'config' CONF_WEBCOMPONENT_PATH = 'webcomponent_path' +CONF_JS_URL = 'js_url' +CONF_EMBED_IFRAME = 'embed_iframe' +CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' DEFAULT_ICON = 'mdi:bookmark' +LEGACY_URL = '/api/panel_custom/{}' PANEL_DIR = 'panels' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [{ - vol.Required(CONF_COMPONENT_NAME): cv.slug, + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_COMPONENT_NAME): cv.string, vol.Optional(CONF_SIDEBAR_TITLE): cv.string, vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): cv.match_all, + vol.Optional(CONF_CONFIG): dict, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, - }]) + vol.Optional(CONF_JS_URL): cv.string, + vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean, + vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean, + })]) }, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize custom panel.""" success = False @@ -52,17 +57,39 @@ def async_setup(hass, config): if panel_path is None: panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) - if not os.path.isfile(panel_path): + custom_panel_config = { + 'name': name, + 'embed_iframe': panel[CONF_EMBED_IFRAME], + 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], + } + + if CONF_JS_URL in panel: + custom_panel_config['js_url'] = panel[CONF_JS_URL] + + elif not await hass.async_add_job(os.path.isfile, panel_path): _LOGGER.error('Unable to find webcomponent for %s: %s', name, panel_path) continue - yield from hass.components.frontend.async_register_panel( - name, panel_path, + else: + url = LEGACY_URL.format(name) + hass.http.register_static_path(url, panel_path) + custom_panel_config['html_url'] = LEGACY_URL.format(name) + + if CONF_CONFIG in panel: + # Make copy because we're mutating it + config = dict(panel[CONF_CONFIG]) + else: + config = {} + + config['_panel_custom'] = custom_panel_config + + await hass.components.frontend.async_register_built_in_panel( + component_name='custom', sidebar_title=panel.get(CONF_SIDEBAR_TITLE), sidebar_icon=panel.get(CONF_SIDEBAR_ICON), frontend_url_path=panel.get(CONF_URL_PATH), - config=panel.get(CONF_CONFIG), + config=config ) success = True diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index 1d33740d4a4..bbc6e07f2b0 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b3'] +REQUIREMENTS = ['restrictedpython==4.0b4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py index 76dda6fd366..bbce7f752af 100644 --- a/homeassistant/components/rainbird.py +++ b/homeassistant/components/rainbird.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_HOST, CONF_PASSWORD) -REQUIREMENTS = ['pyrainbird==0.1.3'] +REQUIREMENTS = ['pyrainbird==0.1.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 505c3a7b2b0..308a945e942 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -168,7 +168,6 @@ class RainCloudEntity(Entity): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'identifier': self.data.serial, } diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py deleted file mode 100644 index f2d5893d60b..00000000000 --- a/homeassistant/components/rainmachine.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -This component provides support for RainMachine sprinkler controllers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rainmachine/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_SWITCHES) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['regenmaschine==0.4.1'] - -_LOGGER = logging.getLogger(__name__) - -DATA_RAINMACHINE = 'data_rainmachine' -DOMAIN = 'rainmachine' - -NOTIFICATION_ID = 'rainmachine_notification' -NOTIFICATION_TITLE = 'RainMachine Component Setup' - -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' -DEFAULT_ICON = 'mdi:water' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True - -PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) - -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_ZONE_RUN_TIME): - cv.positive_int -}) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, - }) - }, - extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import HTTPError - from requests.exceptions import ConnectTimeout - - conf = config[DOMAIN] - ip_address = conf[CONF_IP_ADDRESS] - password = conf[CONF_PASSWORD] - port = conf[CONF_PORT] - ssl = conf[CONF_SSL] - - _LOGGER.debug('Setting up RainMachine client') - - try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - client = Client(auth) - hass.data[DATA_RAINMACHINE] = RainMachine(client) - except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: - _LOGGER.error('An error occurred: %s', str(exc_info)) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(exc_info), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - _LOGGER.debug('Setting up switch platform') - switch_config = conf.get(CONF_SWITCHES, {}) - discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) - - _LOGGER.debug('Setup complete') - - return True - - -class RainMachine(object): - """Define a generic RainMachine object.""" - - def __init__(self, client): - """Initialize.""" - self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] - - -class RainMachineEntity(Entity): - """Define a generic RainMachine entity.""" - - def __init__(self, - rainmachine, - rainmachine_type, - rainmachine_entity_id, - icon=DEFAULT_ICON): - """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = icon - self._rainmachine_type = rainmachine_type - self._rainmachine_entity_id = rainmachine_entity_id - self.rainmachine = rainmachine - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}_{2}'.format( - self.rainmachine.device_mac.replace( - ':', ''), self._rainmachine_type, - self._rainmachine_entity_id) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py new file mode 100644 index 00000000000..7ee6b063720 --- /dev/null +++ b/homeassistant/components/rainmachine/__init__.py @@ -0,0 +1,226 @@ +""" +Support for RainMachine devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, + CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['regenmaschine==0.4.2'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) + +CONF_PROGRAM_ID = 'program_id' +CONF_ZONE_ID = 'zone_id' +CONF_ZONE_RUN_TIME = 'zone_run_time' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' +DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_SSL = True +DEFAULT_ZONE_RUN = 60 * 10 + +TYPE_FREEZE = 'freeze' +TYPE_FREEZE_PROTECTION = 'freeze_protection' +TYPE_FREEZE_TEMP = 'freeze_protect_temp' +TYPE_HOT_DAYS = 'extra_water_on_hot_days' +TYPE_HOURLY = 'hourly' +TYPE_MONTH = 'month' +TYPE_RAINDELAY = 'raindelay' +TYPE_RAINSENSOR = 'rainsensor' +TYPE_WEEKDAY = 'weekday' + +BINARY_SENSORS = { + TYPE_FREEZE: ('Freeze Restrictions', 'mdi:cancel'), + TYPE_FREEZE_PROTECTION: ('Freeze Protection', 'mdi:weather-snowy'), + TYPE_HOT_DAYS: ('Extra Water on Hot Days', 'mdi:thermometer-lines'), + TYPE_HOURLY: ('Hourly Restrictions', 'mdi:cancel'), + TYPE_MONTH: ('Month Restrictions', 'mdi:cancel'), + TYPE_RAINDELAY: ('Rain Delay Restrictions', 'mdi:cancel'), + TYPE_RAINSENSOR: ('Rain Sensor Restrictions', 'mdi:cancel'), + TYPE_WEEKDAY: ('Weekday Restrictions', 'mdi:cancel'), +} + +SENSORS = { + TYPE_FREEZE_TEMP: ('Freeze Protect Temperature', 'mdi:thermometer', '°C'), +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_START_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_START_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): + cv.positive_int, +}) + +SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_STOP_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, +}) + +SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int}) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: + vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import RainMachineError + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + rainmachine = RainMachine(hass, Client(auth)) + rainmachine.update() + hass.data[DATA_RAINMACHINE] = rainmachine + except RainMachineError as exc: + _LOGGER.error('An error occurred: %s', str(exc)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component, schema in [ + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('sensor', conf[CONF_SENSORS]), + ('switch', conf[CONF_SWITCHES]), + ]: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def refresh(event_time): + """Refresh RainMachine data.""" + _LOGGER.debug('Updating RainMachine data') + hass.data[DATA_RAINMACHINE].update() + dispatcher_send(hass, DATA_UPDATE_TOPIC) + + track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + + def start_program(service): + """Start a particular program.""" + rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + + def start_zone(service): + """Start a particular zone for a certain amount of time.""" + rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + + def stop_all(service): + """Stop all watering.""" + rainmachine.client.watering.stop_all() + + def stop_program(service): + """Stop a program.""" + rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + + def stop_zone(service): + """Stop a zone.""" + rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + + for service, method, schema in [ + ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), + ('start_zone', start_zone, SERVICE_START_ZONE_SCHEMA), + ('stop_all', stop_all, {}), + ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), + ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) + ]: + hass.services.register(DOMAIN, service, method, schema=schema) + + return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, hass, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + self.restrictions = {} + + def update(self): + """Update sensor/binary sensor data.""" + self.restrictions.update({ + 'current': self.client.restrictions.current(), + 'global': self.client.restrictions.universal() + }) + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, rainmachine): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = None + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml new file mode 100644 index 00000000000..a8c77628c8f --- /dev/null +++ b/homeassistant/components/rainmachine/services.yaml @@ -0,0 +1,32 @@ +# Describes the format for available RainMachine services + +--- +start_program: + description: Start a program. + fields: + program_id: + description: The program to start. + example: 3 +start_zone: + description: Start a zone for a set number of seconds. + fields: + zone_id: + description: The zone to start. + example: 3 + zone_run_time: + description: The number of seconds to run the zone. + example: 120 +stop_all: + description: Stop all watering activities. +stop_program: + description: Stop a program. + fields: + program_id: + description: The program to stop. + example: 3 +stop_zone: + description: Stop a zone. + fields: + zone_id: + description: The zone to stop. + example: 3 diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9b5bea043f4..38ba593261f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 8bed72a67c2..38d2226012c 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['blockchain==1.4.0'] +REQUIREMENTS = ['blockchain==1.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index d6764e5e994..5cec528d26a 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -28,6 +28,12 @@ import homeassistant.helpers.config_validation as cv _RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json' _LOGGER = logging.getLogger(__name__) +ATTR_LAST_UPDATE = 'last_update' +ATTR_SENSOR_ID = 'sensor_id' +ATTR_STATION_ID = 'station_id' +ATTR_STATION_NAME = 'station_name' +ATTR_ZONE_ID = 'zone_id' + CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' @@ -35,7 +41,6 @@ CONF_WMO_ID = 'wmo_id' MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) -# Sensor types are defined like: Name, units SENSOR_TYPES = { 'wmo': ['wmo', None], 'name': ['Station Name', None], @@ -70,7 +75,7 @@ SENSOR_TYPES = { 'weather': ['Weather', None], 'wind_dir': ['Wind Direction', None], 'wind_spd_kmh': ['Wind Speed kmh', 'km/h'], - 'wind_spd_kt': ['Wind Direction kt', 'kt'] + 'wind_spd_kt': ['Wind Speed kt', 'kt'] } @@ -98,6 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BOM sensor.""" station = config.get(CONF_STATION) zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID) + if station is not None: if zone_id and wmo_id: _LOGGER.warning( @@ -111,17 +117,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.config.config_dir) if station is None: _LOGGER.error("Could not get BOM weather station from lat/lon") - return False + return bom_data = BOMCurrentData(hass, station) + try: bom_data.update() except ValueError as err: - _LOGGER.error("Received error from BOM_Current: %s", err) - return False + _LOGGER.error("Received error from BOM Current: %s", err) + return + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) - return True class BOMCurrentSensor(Entity): @@ -150,14 +157,17 @@ class BOMCurrentSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.latest_data['history_product'] - attr['Station Id'] = self.bom_data.latest_data['wmo'] - attr['Station Name'] = self.bom_data.latest_data['name'] - attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_LAST_UPDATE: datetime.datetime.strptime( + str(self.bom_data.latest_data['local_date_time_full']), + '%Y%m%d%H%M%S'), + ATTR_SENSOR_ID: self._condition, + ATTR_STATION_ID: self.bom_data.latest_data['wmo'], + ATTR_STATION_NAME: self.bom_data.latest_data['name'], + ATTR_ZONE_ID: self.bom_data.latest_data['history_product'], + } + return attr @property @@ -180,8 +190,9 @@ class BOMCurrentData(object): self._data = None def _build_url(self): + """Build the URL for the requests.""" url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) - _LOGGER.info("BOM URL %s", url) + _LOGGER.debug("BOM URL: %s", url) return url @property @@ -200,7 +211,7 @@ class BOMCurrentData(object): for the latest value that is not `-`. Iterators are used in this method to avoid iterating needlessly - iterating through the entire BOM provided dataset + iterating through the entire BOM provided dataset. """ condition_readings = (entry[condition] for entry in self._data) return next((x for x in condition_readings if x != '-'), None) @@ -257,7 +268,7 @@ def _get_bom_stations(): def bom_stations(cache_dir): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. - Results from internet requests are cached as compressed json, making + Results from internet requests are cached as compressed JSON, making subsequent calls very much faster. """ cache_file = os.path.join(cache_dir, '.bom-stations.json.gz') @@ -277,7 +288,7 @@ def closest_station(lat, lon, cache_dir): stations = bom_stations(cache_dir) def comparable_dist(wmo_id): - """Create a psudeo-distance from lat/lon.""" + """Create a psudeo-distance from latitude/longitude.""" station_lat, station_lon = stations[wmo_id] return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 849e21a0901..f4b666f1e5c 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -13,64 +13,78 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) + ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==4.2.1'] +REQUIREMENTS = ['coinmarketcap==5.0.3'] _LOGGER = logging.getLogger(__name__) -ATTR_24H_VOLUME = '24h_volume' +ATTR_VOLUME_24H = 'volume_24h' ATTR_AVAILABLE_SUPPLY = 'available_supply' +ATTR_CIRCULATING_SUPPLY = 'circulating_supply' ATTR_MARKET_CAP = 'market_cap' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PRICE = 'price' +ATTR_RANK = 'rank' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' CONF_ATTRIBUTION = "Data provided by CoinMarketCap" +CONF_CURRENCY_ID = 'currency_id' +CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' -DEFAULT_CURRENCY = 'bitcoin' +DEFAULT_CURRENCY_ID = 1 DEFAULT_DISPLAY_CURRENCY = 'USD' +DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2 ICON = 'mdi:currency-usd' SCAN_INTERVAL = timedelta(minutes=15) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, + vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): + cv.positive_int, vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): cv.string, + vol.Optional(CONF_DISPLAY_CURRENCY_DECIMALS, + default=DEFAULT_DISPLAY_CURRENCY_DECIMALS): + vol.All(vol.Coerce(int), vol.Range(min=1)), }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CoinMarketCap sensor.""" - currency = config.get(CONF_CURRENCY) - display_currency = config.get(CONF_DISPLAY_CURRENCY).lower() + currency_id = config.get(CONF_CURRENCY_ID) + display_currency = config.get(CONF_DISPLAY_CURRENCY).upper() + display_currency_decimals = config.get(CONF_DISPLAY_CURRENCY_DECIMALS) try: - CoinMarketCapData(currency, display_currency).update() + CoinMarketCapData(currency_id, display_currency).update() except HTTPError: - _LOGGER.warning("Currency %s or display currency %s is not available. " - "Using bitcoin and USD.", currency, display_currency) - currency = DEFAULT_CURRENCY + _LOGGER.warning("Currency ID %s or display currency %s " + "is not available. Using 1 (bitcoin) " + "and USD.", currency_id, display_currency) + currency_id = DEFAULT_CURRENCY_ID display_currency = DEFAULT_DISPLAY_CURRENCY add_devices([CoinMarketCapSensor( - CoinMarketCapData(currency, display_currency))], True) + CoinMarketCapData( + currency_id, display_currency), display_currency_decimals)], True) class CoinMarketCapSensor(Entity): """Representation of a CoinMarketCap sensor.""" - def __init__(self, data): + def __init__(self, data, display_currency_decimals): """Initialize the sensor.""" self.data = data + self.display_currency_decimals = display_currency_decimals self._ticker = None - self._unit_of_measurement = self.data.display_currency.upper() + self._unit_of_measurement = self.data.display_currency @property def name(self): @@ -80,8 +94,9 @@ class CoinMarketCapSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return round(float(self._ticker.get( - 'price_{}'.format(self.data.display_currency))), 2) + return round(float( + self._ticker.get('quotes').get(self.data.display_currency) + .get('price')), self.display_currency_decimals) @property def unit_of_measurement(self): @@ -97,15 +112,24 @@ class CoinMarketCapSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_24H_VOLUME: self._ticker.get( - '24h_volume_{}'.format(self.data.display_currency)), + ATTR_VOLUME_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('volume_24h'), ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), - ATTR_MARKET_CAP: self._ticker.get( - 'market_cap_{}'.format(self.data.display_currency)), - ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), - ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), - ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), + ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), + ATTR_MARKET_CAP: + self._ticker.get('quotes').get(self.data.display_currency) + .get('market_cap'), + ATTR_PERCENT_CHANGE_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_24h'), + ATTR_PERCENT_CHANGE_7D: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_7d'), + ATTR_PERCENT_CHANGE_1H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_1h'), + ATTR_RANK: self._ticker.get('rank'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } @@ -113,20 +137,20 @@ class CoinMarketCapSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._ticker = self.data.ticker[0] + self._ticker = self.data.ticker.get('data') class CoinMarketCapData(object): """Get the latest data and update the states.""" - def __init__(self, currency, display_currency): + def __init__(self, currency_id, display_currency): """Initialize the data object.""" - self.currency = currency + self.currency_id = currency_id self.display_currency = display_currency self.ticker = None def update(self): - """Get the latest data from blockchain.info.""" + """Get the latest data from coinmarketcap.com.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, limit=1, convert=self.display_currency) + self.currency_id, convert=self.display_currency) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 221cdf2129e..0db06622ad8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,7 +5,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -33,14 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add sensors from deCONZ.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): if sensor.type in DECONZ_REMOTE: if sensor.battery: entities.append(DeconzBattery(sensor)) else: entities.append(DeconzSensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -114,9 +118,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + from pydeconz.sensor import LIGHTLEVEL attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: + attr['dark'] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index aca2d7bdb9a..d7b867081a3 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/sensor.ebox/ import logging from datetime import timedelta -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -18,9 +17,11 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_MONITORED_VARIABLES) from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady -# pylint: disable=import-error -REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10 + +REQUIREMENTS = ['pyebox==1.1.4'] _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,8 @@ PERCENT = '%' # type: str DEFAULT_NAME = 'EBox' REQUESTS_TIMEOUT = 15 -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { 'usage': ['Usage', PERCENT, 'mdi:percent'], @@ -62,25 +64,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the EBox sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - try: - ebox_data = EBoxData(username, password) - ebox_data.update() - except requests.exceptions.HTTPError as error: - _LOGGER.error("Failed login: %s", error) - return False + httpsession = hass.helpers.aiohttp_client.async_get_clientsession() + ebox_data = EBoxData(username, password, httpsession) name = config.get(CONF_NAME) + from pyebox.client import PyEboxError + try: + await ebox_data.async_update() + except PyEboxError as exp: + _LOGGER.error("Failed login: %s", exp) + raise PlatformNotReady + sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(EBoxSensor(ebox_data, variable, name)) - add_devices(sensors, True) + async_add_devices(sensors, True) class EBoxSensor(Entity): @@ -116,9 +122,9 @@ class EBoxSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - def update(self): + async def async_update(self): """Get the latest data from EBox and update the state.""" - self.ebox_data.update() + await self.ebox_data.async_update() if self.type in self.ebox_data.data: self._state = round(self.ebox_data.data[self.type], 2) @@ -126,18 +132,21 @@ class EBoxSensor(Entity): class EBoxData(object): """Get data from Ebox.""" - def __init__(self, username, password): + def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT) + self.client = EboxClient(username, password, + REQUESTS_TIMEOUT, httpsession) self.data = {} - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from Ebox.""" from pyebox.client import PyEboxError try: - self.client.fetch_data() + await self.client.fetch_data() except PyEboxError as exp: _LOGGER.error("Error on receive last EBox data: %s", exp) return + # Update data self.data = self.client.get_data() diff --git a/homeassistant/components/sensor/gitter.py b/homeassistant/components/sensor/gitter.py index 58f33635750..907af07a2db 100644 --- a/homeassistant/components/sensor/gitter.py +++ b/homeassistant/components/sensor/gitter.py @@ -8,12 +8,12 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, CONF_ROOM +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['gitterpy==0.1.6'] +REQUIREMENTS = ['gitterpy==0.1.7'] _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = gitter.auth.get_my_id['name'] except GitterTokenError: _LOGGER.error("Token is not valid") - return False + return add_devices([GitterSensor(gitter, room, name, username)], True) @@ -96,7 +96,14 @@ class GitterSensor(Entity): def update(self): """Get the latest data and updates the state.""" - data = self._data.user.unread_items(self._room) + from gitterpy.errors import GitterRoomError + + try: + data = self._data.user.unread_items(self._room) + except GitterRoomError as error: + _LOGGER.error(error) + return + if 'error' not in data.keys(): self._mention = len(data['mention']) self._state = len(data['chat']) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 3b6f3ddc99d..0de87bd17ea 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -161,7 +161,8 @@ class GlancesSensor(Entity): elif self.type == 'docker_active': count = 0 for container in value['docker']['containers']: - if container['Status'] == 'running': + if container['Status'] == 'running' or \ + 'Up' in container['Status']: count += 1 self._state = count elif self.type == 'docker_cpu_use': diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index aa350f7be5d..ccd1949cc3b 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -10,7 +10,9 @@ import logging from homeassistant.components.homematicip_cloud import ( HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, ATTR_HOME_ID) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE) _LOGGER = logging.getLogger(__name__) @@ -36,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_devices, """Set up the HomematicIP sensors devices.""" from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, - TemperatureHumiditySensorDisplay) + TemperatureHumiditySensorDisplay, MotionDetectorIndoor) if discovery_info is None: return @@ -50,6 +52,8 @@ async def async_setup_platform(hass, config, async_add_devices, TemperatureHumiditySensorWithoutDisplay)): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) + if isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipIlluminanceSensor(home, device)) if devices: async_add_devices(devices) @@ -149,6 +153,11 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Humidity') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_HUMIDITY + @property def icon(self): """Return the icon.""" @@ -172,6 +181,11 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Temperature') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + @property def icon(self): """Return the icon.""" @@ -186,3 +200,26 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS + + +class HomematicipIlluminanceSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Illuminance') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state.""" + return self._device.illumination + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'lx' diff --git a/homeassistant/components/sensor/hydrawise.py b/homeassistant/components/sensor/hydrawise.py new file mode 100644 index 00000000000..fea2780da07 --- /dev/null +++ b/homeassistant/components/sensor/hydrawise.py @@ -0,0 +1,72 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for zone in hydrawise.relays: + sensors.append(HydrawiseSensor(zone, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSensor(HydrawiseEntity): + """A sensor implementation for Hydrawise device.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise sensor: %s", self._name) + if self._sensor_type == 'watering_time': + if not mydata.running: + self._state = 0 + else: + if int(mydata.running[0]['relay']) == self.data['relay']: + self._state = int(mydata.running[0]['time_left']/60) + else: + self._state = 0 + else: # _sensor_type == 'next_cycle' + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay['nicetime'] == 'Not scheduled': + self._state = 'not_scheduled' + else: + self._state = relay['nicetime'].split(',')[0] + \ + ' ' + relay['nicetime'].split(' ')[3] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py new file mode 100644 index 00000000000..8e030390f50 --- /dev/null +++ b/homeassistant/components/sensor/iperf3.py @@ -0,0 +1,195 @@ +""" +Support for Iperf3 network measurement tool. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.iperf3/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, + CONF_HOST, CONF_PORT, CONF_PROTOCOL) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['iperf3==0.1.10'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' +ATTR_VERSION = 'Version' + +CONF_ATTRIBUTION = 'Data retrieved using Iperf3' +CONF_DURATION = 'duration' +CONF_PARALLEL = 'parallel' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 +DEFAULT_PARALLEL = 1 +DEFAULT_PROTOCOL = 'tcp' + +IPERF3_DATA = 'iperf3' + +SCAN_INTERVAL = timedelta(minutes=60) + +SERVICE_NAME = 'iperf3_update' + +ICON = 'mdi:speedometer' + +SENSOR_TYPES = { + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), + vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): + vol.In(['tcp', 'udp']), +}) + + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Iperf3 sensor.""" + if hass.data.get(IPERF3_DATA) is None: + hass.data[IPERF3_DATA] = {} + hass.data[IPERF3_DATA]['sensors'] = [] + + dev = [] + for sensor in config[CONF_MONITORED_CONDITIONS]: + dev.append( + Iperf3Sensor(config[CONF_HOST], + config[CONF_PORT], + config[CONF_DURATION], + config[CONF_PARALLEL], + config[CONF_PROTOCOL], + sensor)) + + hass.data[IPERF3_DATA]['sensors'].extend(dev) + add_devices(dev) + + def _service_handler(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] + + for sensor in all_iperf3_sensors: + if entity_id is not None: + if sensor.entity_id == entity_id: + sensor.update() + sensor.schedule_update_ha_state() + break + else: + sensor.update() + sensor.schedule_update_ha_state() + + for sensor in dev: + hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, + schema=SERVICE_SCHEMA) + + +class Iperf3Sensor(Entity): + """A Iperf3 sensor implementation.""" + + def __init__(self, server, port, duration, streams, + protocol, sensor_type): + """Initialize the sensor.""" + self._attrs = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_PROTOCOL: protocol, + } + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], server) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._port = port + self._server = server + self._duration = duration + self._num_streams = streams + self._protocol = protocol + self.result = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.result is not None: + self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host + self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port + self._attrs[ATTR_VERSION] = self.result.version + return self._attrs + + def update(self): + """Get the latest data and update the states.""" + import iperf3 + client = iperf3.Client() + client.duration = self._duration + client.server_hostname = self._server + client.port = self._port + client.verbose = False + client.num_streams = self._num_streams + client.protocol = self._protocol + + # when testing download bandwith, reverse must be True + if self._sensor_type == 'download': + client.reverse = True + + try: + self.result = client.run() + except (AttributeError, OSError, ValueError) as error: + self.result = None + _LOGGER.error("Iperf3 sensor error: %s", error) + return + + if self.result is not None and \ + hasattr(self.result, 'error') and \ + self.result.error is not None: + _LOGGER.error("Iperf3 sensor error: %s", self.result.error) + self.result = None + return + + # UDP only have 1 way attribute + if self._protocol == 'udp': + self._state = round(self.result.Mbps, 2) + + elif self._sensor_type == 'download': + self._state = round(self.result.received_Mbps, 2) + + elif self._sensor_type == 'upload': + self._state = round(self.result.sent_Mbps, 2) + + @property + def icon(self): + """Return icon.""" + return ICON diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index c5e0b12b0e0..9952e2a1c24 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -4,7 +4,6 @@ Support for Luftdaten sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.luftdaten/ """ -import asyncio from datetime import timedelta import logging @@ -19,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['luftdaten==0.1.3'] +REQUIREMENTS = ['luftdaten==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -59,8 +58,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Luftdaten sensor.""" from luftdaten import Luftdaten @@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): session = async_get_clientsession(hass) luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session)) - yield from luftdaten.async_update() + await luftdaten.async_update() if luftdaten.data is None: _LOGGER.error("Sensor is not available: %s", sensor_id) diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index ecea0815e79..f6bec3284c3 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -33,6 +33,7 @@ DIGITS = { SENSOR_MODELS = [ 'Ubiquiti mFi-THS', 'Ubiquiti mFi-CS', + 'Ubiquiti mFi-DS', 'Outlet', 'Input Analog', 'Input Digital', diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 9ce50dc61e5..00d18c7fe10 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -4,42 +4,44 @@ Support for Nest Thermostat Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ -from itertools import chain import logging -from homeassistant.components.nest import DATA_NEST -from homeassistant.helpers.entity import Entity +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) DEPENDENCIES = ['nest'] -SENSOR_TYPES = ['humidity', - 'operation_mode', - 'hvac_state'] + +SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] + +TEMP_SENSOR_TYPES = ['temperature', 'target'] + +PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health'] + +STRUCTURE_SENSOR_TYPES = ['eta'] + +_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ + + STRUCTURE_SENSOR_TYPES + +SENSOR_UNITS = {'humidity': '%'} + +SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY} + +VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} SENSOR_TYPES_DEPRECATED = ['last_ip', 'local_ip', - 'last_connection'] + 'last_connection', + 'battery_level'] -DEPRECATED_WEATHER_VARS = {'weather_humidity': 'humidity', - 'weather_temperature': 'temperature', - 'weather_condition': 'condition', - 'wind_speed': 'kph', - 'wind_direction': 'direction'} +DEPRECATED_WEATHER_VARS = ['weather_humidity', + 'weather_temperature', + 'weather_condition', + 'wind_speed', + 'wind_direction'] -SENSOR_UNITS = {'humidity': '%', 'temperature': '°C'} - -PROTECT_VARS = ['co_status', 'smoke_status', 'battery_health'] - -PROTECT_VARS_DEPRECATED = ['battery_level'] - -SENSOR_TEMP_TYPES = ['temperature', 'target'] - -_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ - + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED - -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS +_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS _LOGGER = logging.getLogger(__name__) @@ -69,53 +71,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "monitored_conditions. See " "https://home-assistant.io/components/" "binary_sensor.nest/ for valid options.") - _LOGGER.error(wstr) all_sensors = [] - for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): - sensors = [NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TYPES and device.is_thermostat] - sensors += [NestTempSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TEMP_TYPES and device.is_thermostat] - sensors += [NestProtectSensor(structure, device, variable) - for variable in conditions - if variable in PROTECT_VARS and device.is_smoke_co_alarm] - all_sensors.extend(sensors) + for structure in nest.structures(): + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_SENSOR_TYPES] + + for structure, device in nest.thermostats(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in SENSOR_TYPES] + all_sensors += [NestTempSensor(structure, device, variable) + for variable in conditions + if variable in TEMP_SENSOR_TYPES] + + for structure, device in nest.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] add_devices(all_sensors, True) -class NestSensor(Entity): - """Representation of a Nest sensor.""" - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.device = device - self.variable = variable - - # device specific - self._location = self.device.where - self._name = "{} {}".format(self.device.name_long, - self.variable.replace("_", " ")) - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - -class NestBasicSensor(NestSensor): +class NestBasicSensor(NestSensorDevice): """Representation a basic Nest sensor.""" @property @@ -123,17 +103,26 @@ class NestBasicSensor(NestSensor): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_DEVICE_CLASSES.get(self.variable) + def update(self): """Retrieve latest state.""" - self._unit = SENSOR_UNITS.get(self.variable, None) + self._unit = SENSOR_UNITS.get(self.variable) - if self.variable == 'operation_mode': - self._state = getattr(self.device, "mode") + if self.variable in VARIABLE_NAME_MAPPING: + self._state = getattr(self.device, + VARIABLE_NAME_MAPPING[self.variable]) + elif self.variable in PROTECT_SENSOR_TYPES: + # keep backward compatibility + self._state = getattr(self.device, self.variable).capitalize() else: self._state = getattr(self.device, self.variable) -class NestTempSensor(NestSensor): +class NestTempSensor(NestSensorDevice): """Representation of a Nest Temperature sensor.""" @property @@ -162,16 +151,3 @@ class NestTempSensor(NestSensor): self._state = "%s-%s" % (int(low), int(high)) else: self._state = round(temp, 1) - - -class NestProtectSensor(NestSensor): - """Return the state of nest protect.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Retrieve latest state.""" - self._state = getattr(self.device, self.variable).capitalize() diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4aeba082e55..f09e1d4f395 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -10,7 +10,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + STATE_UNKNOWN) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -26,28 +28,29 @@ DEPENDENCIES = ['netatmo'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'co2': ['CO2', 'ppm', 'mdi:cloud'], - 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], - 'noise': ['Noise', 'dB', 'mdi:volume-high'], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], - 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], - 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], - 'battery_vp': ['Battery', '', 'mdi:battery'], - 'battery_lvl': ['Battery_lvl', '', 'mdi:battery'], - 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'windangle': ['Angle', '', 'mdi:compass'], - 'windangle_value': ['Angle Value', 'º', 'mdi:compass'], - 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy'], - 'gustangle': ['Gust Angle', '', 'mdi:compass'], - 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass'], - 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], - 'rf_status': ['Radio', '', 'mdi:signal'], - 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'], - 'wifi_status': ['Wifi', '', 'mdi:wifi'], - 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi'] + 'temperature': ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + 'co2': ['CO2', 'ppm', 'mdi:cloud', None], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'noise': ['Noise', 'dB', 'mdi:volume-high', None], + 'humidity': ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy', None], + 'battery_vp': ['Battery', '', 'mdi:battery', None], + 'battery_lvl': ['Battery_lvl', '', 'mdi:battery', None], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'windangle': ['Angle', '', 'mdi:compass', None], + 'windangle_value': ['Angle Value', 'º', 'mdi:compass', None], + 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy', None], + 'gustangle': ['Gust Angle', '', 'mdi:compass', None], + 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], + 'rf_status': ['Radio', '', 'mdi:signal', None], + 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], + 'wifi_status': ['Wifi', '', 'mdi:wifi', None], + 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None] } MODULE_SCHEMA = vol.Schema({ @@ -106,7 +109,9 @@ class NetAtmoSensor(Entity): self.module_name = module_name self.type = sensor_type self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] @@ -119,7 +124,12 @@ class NetAtmoSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class @property def state(self): diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index 0d2a542c7bb..2d08159967c 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -4,154 +4,152 @@ Support gathering system information of hosts which are running netdata. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.netdata/ """ -import logging from datetime import timedelta -from urllib.parse import urlsplit +import logging -import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, CONF_RESOURCES) + CONF_HOST, CONF_ICON, CONF_NAME, CONF_PORT, CONF_RESOURCES) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['netdata==0.1.2'] _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'api/v1' -_REALTIME = 'before=0&after=-1&options=seconds' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +CONF_ELEMENT = 'element' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Netdata' -DEFAULT_PORT = '19999' +DEFAULT_PORT = 19999 -SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_ICON = 'mdi:desktop-classic' -SENSOR_TYPES = { - 'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1], - 'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1], - 'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1], - 'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1], - 'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1], - 'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1], - 'processes_running': ['Processes Running', 'Count', 'system.processes', - 'running', 0], - 'processes_blocked': ['Processes Blocked', 'Count', 'system.processes', - 'blocked', 0], - 'system_load': ['System Load', '15 min', 'system.load', 'load15', 2], - 'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0], - 'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0], - 'ipv4_in': ['IPv4 In', 'kb/s', 'system.ipv4', 'received', 0], - 'ipv4_out': ['IPv4 Out', 'kb/s', 'system.ipv4', 'sent', 0], - 'disk_free': ['Disk Free', 'GiB', 'disk_space._', 'avail', 2], - 'cpu_iowait': ['CPU IOWait', '%', 'system.cpu', 'iowait', 1], - 'cpu_user': ['CPU User', '%', 'system.cpu', 'user', 1], - 'cpu_system': ['CPU System', '%', 'system.cpu', 'system', 1], - 'cpu_softirq': ['CPU SoftIRQ', '%', 'system.cpu', 'softirq', 1], - 'cpu_guest': ['CPU Guest', '%', 'system.cpu', 'guest', 1], - 'uptime': ['Uptime', 's', 'system.uptime', 'uptime', 0], - 'packets_received': ['Packets Received', 'packets/s', 'ipv4.packets', - 'received', 0], - 'packets_sent': ['Packets Sent', 'packets/s', 'ipv4.packets', - 'sent', 0], - 'connections': ['Active Connections', 'Count', - 'netfilter.conntrack_sockets', 'connections', 0] -} +RESOURCE_SCHEMA = vol.Any({ + vol.Required(CONF_ELEMENT): cv.string, + vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_NAME): cv.string, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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, - vol.Optional(CONF_RESOURCES, default=['memory_free']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_RESOURCES): vol.Schema({cv.string: RESOURCE_SCHEMA}), }) -# pylint: disable=unused-variable -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Netdata sensor.""" + from netdata import Netdata + name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = 'http://{}:{}'.format(host, port) - data_url = '{}/{}/data?chart='.format(url, _RESOURCE) resources = config.get(CONF_RESOURCES) - values = {} - for key, value in sorted(SENSOR_TYPES.items()): - if key in resources: - values.setdefault(value[2], []).append(key) + session = async_get_clientsession(hass) + netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + await netdata.async_update() + + if netdata.api.metrics is None: + raise PlatformNotReady dev = [] - for chart in values: - rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME) - rest = NetdataData(rest_url) - rest.update() - for sensor_type in values[chart]: - dev.append(NetdataSensor(rest, name, sensor_type)) + for entry, data in resources.items(): + sensor = entry + element = data[CONF_ELEMENT] + sensor_name = icon = None + try: + resource_data = netdata.api.metrics[sensor] + unit = '%' if resource_data['units'] == 'percentage' else \ + resource_data['units'] + if data is not None: + sensor_name = data.get(CONF_NAME) + icon = data.get(CONF_ICON) + except KeyError: + _LOGGER.error("Sensor is not available: %s", sensor) + continue - add_devices(dev, True) + dev.append(NetdataSensor( + netdata, name, sensor, sensor_name, element, icon, unit)) + + async_add_devices(dev, True) class NetdataSensor(Entity): """Implementation of a Netdata sensor.""" - def __init__(self, rest, name, sensor_type): + def __init__( + self, netdata, name, sensor, sensor_name, element, icon, unit): """Initialize the Netdata sensor.""" - self.rest = rest - self.type = sensor_type - self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0]) - self._precision = SENSOR_TYPES[self.type][4] - self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self.netdata = netdata + self._state = None + self._sensor = sensor + self._element = element + self._sensor_name = self._sensor if sensor_name is None else \ + sensor_name + self._name = name + self._icon = icon + self._unit_of_measurement = unit @property def name(self): """Return the name of the sensor.""" - return self._name + return '{} {}'.format(self._name, self._sensor_name) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the resources.""" - value = self.rest.data - - if value is not None: - netdata_id = SENSOR_TYPES[self.type][3] - if netdata_id in value: - return "{0:.{1}f}".format(value[netdata_id], self._precision) - return None + return self._state @property def available(self): """Could the resource be accessed during the last update call.""" - return self.rest.available + return self.netdata.available - def update(self): + async def async_update(self): """Get the latest data from Netdata REST API.""" - self.rest.update() + await self.netdata.async_update() + resource_data = self.netdata.api.metrics.get(self._sensor) + self._state = round( + resource_data['dimensions'][self._element]['value'], 2) class NetdataData(object): """The class for handling the data retrieval.""" - def __init__(self, resource): + def __init__(self, api): """Initialize the data object.""" - self._resource = resource - self.data = None + self.api = api self.available = True - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from the Netdata REST API.""" + from netdata.exceptions import NetdataError + try: - response = requests.get(self._resource, timeout=5) - det = response.json() - self.data = {k: v for k, v in zip(det['labels'], det['data'][0])} + await self.api.get_allmetrics() self.available = True - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error: %s", urlsplit(self._resource)[1]) - self.data = None + except NetdataError: + _LOGGER.error("Unable to retrieve data from Netdata") self.available = False diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index b8917080efc..bf440728a2e 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, CONF_RESOURCES, CONF_ALIAS, ATTR_STATE, STATE_UNKNOWN) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -26,10 +27,12 @@ DEFAULT_HOST = 'localhost' DEFAULT_PORT = 3493 KEY_STATUS = 'ups.status' +KEY_STATUS_DISPLAY = 'ups.status.display' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { + 'ups.status.display': ['Status', '', 'mdi:information-outline'], 'ups.status': ['Status Data', '', 'mdi:information-outline'], 'ups.alarm': ['Alarms', '', 'mdi:alarm'], 'ups.time': ['Internal Time', '', 'mdi:calendar-clock'], @@ -130,7 +133,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ALIAS): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_RESOURCES, default=[]): + vol.Required(CONF_RESOURCES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -148,7 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if data.status is None: _LOGGER.error("NUT Sensor has no data, unable to setup") - return False + raise PlatformNotReady _LOGGER.debug('NUT Sensors Available: %s', data.status) @@ -157,7 +160,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for resource in config[CONF_RESOURCES]: sensor_type = resource.lower() - if sensor_type in data.status: + # Display status is a special case that falls back to the status value + # of the UPS instead. + if sensor_type in data.status or (sensor_type == KEY_STATUS_DISPLAY + and KEY_STATUS in data.status): entities.append(NUTSensor(name, data, sensor_type)) else: _LOGGER.warning( @@ -169,7 +175,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except data.pynuterror as err: _LOGGER.error("Failure while testing NUT status retrieval. " "Cannot continue setup: %s", err) - return False + raise PlatformNotReady add_entities(entities, True) @@ -209,11 +215,11 @@ class NUTSensor(Entity): def device_state_attributes(self): """Return the sensor attributes.""" attr = dict() - attr[ATTR_STATE] = self.opp_state() + attr[ATTR_STATE] = self.display_state() return attr - def opp_state(self): - """Return UPS operating state.""" + def display_state(self): + """Return UPS display state.""" if self._data.status is None: return STATE_TYPES['OFF'] else: @@ -230,7 +236,11 @@ class NUTSensor(Entity): self._state = None return - if self.type not in self._data.status: + # In case of the display status sensor, keep a human-readable form + # as the sensor state. + if self.type == KEY_STATUS_DISPLAY: + self._state = self.display_state() + elif self.type not in self._data.status: self._state = None else: self._state = self._data.status[self.type] @@ -288,5 +298,5 @@ class PyNUTData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" + """Fetch the latest status from NUT.""" self._status = self._get_status() diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 8a07d3484d5..43105d54e38 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -28,7 +28,8 @@ DEVICE_SENSORS = {'10': {'temperature': 'temperature'}, '22': {'temperature': 'temperature'}, '26': {'temperature': 'temperature', 'humidity': 'humidity', - 'pressure': 'B1-R1-A/pressure'}, + 'pressure': 'B1-R1-A/pressure', + 'illuminance': 'S3-R1-A/illuminance'}, '28': {'temperature': 'temperature'}, '3B': {'temperature': 'temperature'}, '42': {'temperature': 'temperature'}} @@ -37,6 +38,7 @@ SENSOR_TYPES = { 'temperature': ['temperature', TEMP_CELSIUS], 'humidity': ['humidity', '%'], 'pressure': ['pressure', 'mb'], + 'illuminance': ['illuminance', 'lux'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index c38f58b7916..63a9c1d67d5 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['postnl_api==1.0.1'] +REQUIREMENTS = ['postnl_api==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py new file mode 100644 index 00000000000..8faf30acc38 --- /dev/null +++ b/homeassistant/components/sensor/rainmachine.py @@ -0,0 +1,88 @@ +""" +This platform provides support for sensor data from RainMachine. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rainmachine/ +""" +import logging + +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) + + add_devices(sensors, True) + + +class RainMachineSensor(RainMachineEntity): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon, unit): + """Initialize.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + self._unit = unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self) -> str: + """Return the name of the entity.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the sensor's state.""" + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectTemp'] diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py index e57bbcc3955..c3ff08a5781 100644 --- a/homeassistant/components/sensor/random.py +++ b/homeassistant/components/sensor/random.py @@ -4,7 +4,6 @@ Support for showing random numbers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -34,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Random number sensor.""" name = config.get(CONF_NAME) minimum = config.get(CONF_MINIMUM) @@ -84,8 +83,7 @@ class RandomSensor(Entity): ATTR_MINIMUM: self._minimum, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get a new number and updates the states.""" from random import randrange self._state = randrange(self._minimum, self._maximum + 1) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 720158e1029..bc3e127508b 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.7.7'] +REQUIREMENTS = ['shodan==1.8.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index ae2d4939eab..9dac0b48bc2 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -4,51 +4,48 @@ Adds a simulated sensor. For more details about this platform, refer to the documentation at https://home-assistant.io/components/sensor.simulated/ """ -import asyncio -import datetime as datetime +import logging import math from random import Random -import logging import voluptuous as vol -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(seconds=30) -ICON = 'mdi:chart-line' -CONF_UNIT = 'unit' CONF_AMP = 'amplitude' +CONF_FWHM = 'spread' CONF_MEAN = 'mean' CONF_PERIOD = 'period' CONF_PHASE = 'phase' -CONF_FWHM = 'spread' CONF_SEED = 'seed' +CONF_UNIT = 'unit' -DEFAULT_NAME = 'simulated' -DEFAULT_UNIT = 'value' DEFAULT_AMP = 1 +DEFAULT_FWHM = 0 DEFAULT_MEAN = 0 +DEFAULT_NAME = 'simulated' DEFAULT_PERIOD = 60 DEFAULT_PHASE = 0 -DEFAULT_FWHM = 0 DEFAULT_SEED = 999 +DEFAULT_UNIT = 'value' +ICON = 'mdi:chart-line' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), + vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_MEAN, default=DEFAULT_MEAN): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.positive_int, vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float), - vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, }) @@ -63,9 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fwhm = config.get(CONF_FWHM) seed = config.get(CONF_SEED) - sensor = SimulatedSensor( - name, unit, amp, mean, period, phase, fwhm, seed - ) + sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed) add_devices([sensor], True) @@ -107,8 +102,7 @@ class SimulatedSensor(Entity): noise = self._random.gauss(mu=0, sigma=fwhm) return mean + periodic + noise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the sensor.""" self._state = self.signal_calc() diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 5b03be036d5..bf2868d3b01 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.dt as dt_util -REQUIREMENTS = ['speedtest-cli==2.0.0'] +REQUIREMENTS = ['speedtest-cli==2.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index b7ece1bdb87..7fefb0f450b 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index a489adf6776..928d84caa2b 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -4,7 +4,6 @@ Support for transport.opendata.ch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.swiss_public_transport/ """ -import asyncio from datetime import timedelta import logging @@ -17,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['python_opendata_transport==0.0.3'] +REQUIREMENTS = ['python_opendata_transport==0.1.0'] _LOGGER = logging.getLogger(__name__) @@ -48,8 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Swiss public transport sensor.""" from opendata_transport import OpendataTransport, exceptions @@ -61,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): opendata = OpendataTransport(start, destination, hass.loop, session) try: - yield from opendata.async_get_data() + await opendata.async_get_data() except exceptions.OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " @@ -122,12 +121,11 @@ class SwissPublicTransportSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from opendata.ch and update the states.""" from opendata_transport.exceptions import OpendataTransportError try: - yield from self._opendata.async_get_data() + await self._opendata.async_get_data() except OpendataTransportError: _LOGGER.error("Unable to retrieve data from transport.opendata.ch") diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 678d9afb81d..4dac411d224 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -4,23 +4,23 @@ Support for monitoring the Transmission BitTorrent client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.transmission/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, - CONF_MONITORED_VARIABLES, STATE_IDLE) + CONF_HOST, CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, STATE_IDLE) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady REQUIREMENTS = ['transmissionrpc==0.11'] _LOGGER = logging.getLogger(__name__) -_THROTTLED_REFRESH = None DEFAULT_NAME = 'Transmission' DEFAULT_PORT = 9091 @@ -29,12 +29,16 @@ SENSOR_TYPES = { 'active_torrents': ['Active Torrents', None], 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'MB/s'], + 'paused_torrents': ['Paused Torrents', None], + 'total_torrents': ['Total Torrents', None], 'upload_speed': ['Up Speed', 'MB/s'], } +SCAN_INTERVAL = timedelta(minutes=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): + vol.Optional(CONF_MONITORED_VARIABLES, default=['torrents']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -43,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Transmission sensors.""" import transmissionrpc @@ -56,39 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) try: - transmission_api = transmissionrpc.Client( + transmission = transmissionrpc.Client( host, port=port, user=username, password=password) - transmission_api.session_stats() + transmission_api = TransmissionData(transmission) except TransmissionError as error: - _LOGGER.error( - "Connection to Transmission API failed on %s:%s with message %s", - host, port, error.original - ) - return False + if str(error).find("401: Unauthorized"): + _LOGGER.error("Credentials for Transmission client are not valid") + return - # pylint: disable=global-statement - global _THROTTLED_REFRESH - _THROTTLED_REFRESH = Throttle(timedelta(seconds=1))( - transmission_api.session_stats) + _LOGGER.warning( + "Unable to connect to Transmission client: %s:%s", host, port) + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(TransmissionSensor(variable, transmission_api, name)) - add_devices(dev) + add_devices(dev, True) class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" - def __init__(self, sensor_type, transmission_client, client_name): + def __init__(self, sensor_type, transmission_api, client_name): """Initialize the sensor.""" self._name = SENSOR_TYPES[sensor_type][0] - self.tm_client = transmission_client - self.type = sensor_type - self.client_name = client_name self._state = None + self._transmission_api = transmission_api self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._data = None + self.client_name = client_name + self.type = sensor_type @property def name(self): @@ -105,25 +106,20 @@ class TransmissionSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - # pylint: disable=no-self-use - def refresh_transmission_data(self): - """Call the throttled Transmission refresh method.""" - from transmissionrpc.error import TransmissionError - - if _THROTTLED_REFRESH is not None: - try: - _THROTTLED_REFRESH() - except TransmissionError: - _LOGGER.error("Connection to Transmission API failed") + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available def update(self): """Get the latest data from Transmission and updates the state.""" - self.refresh_transmission_data() + self._transmission_api.update() + self._data = self._transmission_api.data if self.type == 'current_status': - if self.tm_client.session: - upload = self.tm_client.session.uploadSpeed - download = self.tm_client.session.downloadSpeed + if self._data: + upload = self._data.uploadSpeed + download = self._data.downloadSpeed if upload > 0 and download > 0: self._state = 'Up/Down' elif upload > 0 and download == 0: @@ -135,14 +131,40 @@ class TransmissionSensor(Entity): else: self._state = None - if self.tm_client.session: + if self._data: if self.type == 'download_speed': - mb_spd = float(self.tm_client.session.downloadSpeed) + mb_spd = float(self._data.downloadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'upload_speed': - mb_spd = float(self.tm_client.session.uploadSpeed) + mb_spd = float(self._data.uploadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'active_torrents': - self._state = self.tm_client.session.activeTorrentCount + self._state = self._data.activeTorrentCount + elif self.type == 'paused_torrents': + self._state = self._data.pausedTorrentCount + elif self.type == 'total_torrents': + self._state = self._data.torrentCount + + +class TransmissionData(object): + """Get the latest data and update the states.""" + + def __init__(self, api): + """Initialize the Transmission data object.""" + self.data = None + self.available = True + self._api = api + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Transmission instance.""" + from transmissionrpc.error import TransmissionError + + try: + self.data = self._api.session_stats() + self.available = True + except TransmissionError: + self.available = False + _LOGGER.error("Unable to connect to Transmission client") diff --git a/homeassistant/components/sensor/uptime.py b/homeassistant/components/sensor/uptime.py index 91746af71f1..7e893899815 100644 --- a/homeassistant/components/sensor/uptime.py +++ b/homeassistant/components/sensor/uptime.py @@ -1,25 +1,25 @@ """ -Component to retrieve uptime for Home Assistant. +Platform to retrieve uptime for Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uptime/ """ -import asyncio import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Uptime' +ICON = 'mdi:clock' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='days'): @@ -27,22 +27,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the uptime sensor platform.""" name = config.get(CONF_NAME) units = config.get(CONF_UNIT_OF_MEASUREMENT) + async_add_devices([UptimeSensor(name, units)], True) class UptimeSensor(Entity): """Representation of an uptime sensor.""" - def __init__(self, name, units): + def __init__(self, name, unit): """Initialize the uptime sensor.""" self._name = name - self._icon = 'mdi:clock' - self._units = units + self._unit = unit self.initial = dt_util.now() self._state = None @@ -54,27 +54,28 @@ class UptimeSensor(Entity): @property def icon(self): """Icon to display in the front end.""" - return self._icon + return ICON @property def unit_of_measurement(self): """Return the unit of measurement the value is expressed in.""" - return self._units + return self._unit @property def state(self): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state of the sensor.""" delta = dt_util.now() - self.initial div_factor = 3600 + if self.unit_of_measurement == 'days': div_factor *= 24 elif self.unit_of_measurement == 'minutes': div_factor /= 60 + delta = delta.total_seconds() / div_factor self._state = round(delta, 2) _LOGGER.debug("New value: %s", delta) diff --git a/homeassistant/components/sensor/version.py b/homeassistant/components/sensor/version.py index c19d2743563..db61d059783 100644 --- a/homeassistant/components/sensor/version.py +++ b/homeassistant/components/sensor/version.py @@ -4,7 +4,6 @@ Support for displaying the current version of Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.version/ """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Version sensor platform.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 839b5776b3c..1240480d4a3 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -4,7 +4,6 @@ Support for showing the time in a different time zone. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.worldclock/ """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the World clock sensor.""" name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) @@ -62,8 +61,7 @@ class WorldClockSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the time and updates the states.""" self._state = dt_util.now(time_zone=self._time_zone).strftime( TIME_STR_FORMAT) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 3ca908a679d..53e0e8d0329 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -32,8 +32,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement, PressureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement, + IlluminanceMeasurement ) + from zigpy.zcl.clusters.smartenergy import Metering + from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) @@ -41,6 +44,13 @@ def make_sensor(discovery_info): sensor = TemperatureSensor(**discovery_info) elif PressureMeasurement.cluster_id in in_clusters: sensor = PressureSensor(**discovery_info) + elif IlluminanceMeasurement.cluster_id in in_clusters: + sensor = IlluminanceMeasurementSensor(**discovery_info) + elif Metering.cluster_id in in_clusters: + sensor = MeteringSensor(**discovery_info) + elif ElectricalMeasurement.cluster_id in in_clusters: + sensor = ElectricalMeasurementSensor(**discovery_info) + return sensor else: sensor = Sensor(**discovery_info) @@ -104,9 +114,11 @@ class TemperatureSensor(Sensor): """Return the state of the entity.""" if self._state is None: return None - celsius = round(float(self._state) / 100, 1) - return convert_temperature( - celsius, TEMP_CELSIUS, self.unit_of_measurement) + celsius = self._state / 100 + return round(convert_temperature(celsius, + TEMP_CELSIUS, + self.unit_of_measurement), + 1) class RelativeHumiditySensor(Sensor): @@ -143,3 +155,75 @@ class PressureSensor(Sensor): return None return round(float(self._state)) + + +class IlluminanceMeasurementSensor(Sensor): + """ZHA lux sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'lx' + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + +class MeteringSensor(Sensor): + """ZHA Metering sensor.""" + + value_attribute = 1024 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state)) + + +class ElectricalMeasurementSensor(Sensor): + """ZHA Electrical Measurement sensor.""" + + value_attribute = 1291 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def force_update(self) -> bool: + """Force update this entity.""" + return True + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state) / 10, 1) + + @property + def should_poll(self) -> bool: + """Poll state from device.""" + return True + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("%s async_update", self.entity_id) + + result = await zha.safe_read( + self._endpoint.electrical_measurement, + ['active_power'], + allow_cache=False) + self._state = result.get('active_power', self._state) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 746c3c7f483..c0279ef1d0f 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -556,3 +556,17 @@ xiaomi_aqara: device_id: description: Hardware address of the device to remove. example: 158d0000000000 + +shopping_list: + add_item: + description: Adds an item to the shopping list. + fields: + name: + description: The name of the item to add. + example: Beer + complete_item: + description: Marks an item as completed in the shopping list. It does not remove the item. + fields: + name: + description: The name of the item to mark as completed. + example: Beer diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 0ca0fef6e06..f113561429a 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -14,6 +14,8 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +ATTR_NAME = 'name' + DOMAIN = 'shopping_list' DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -23,20 +25,57 @@ INTENT_ADD_ITEM = 'HassShoppingListAddItem' INTENT_LAST_ITEMS = 'HassShoppingListLastItems' ITEM_UPDATE_SCHEMA = vol.Schema({ 'complete': bool, - 'name': str, + ATTR_NAME: str, }) PERSISTENCE = '.shopping_list.json' +SERVICE_ADD_ITEM = 'add_item' +SERVICE_COMPLETE_ITEM = 'complete_item' + +SERVICE_ITEM_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): vol.Any(None, cv.string) +}) + @asyncio.coroutine def async_setup(hass, config): """Initialize the shopping list.""" + @asyncio.coroutine + def add_item_service(call): + """Add an item with `name`.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is not None: + data.async_add(name) + + @asyncio.coroutine + def complete_item_service(call): + """Mark the item provided via `name` as completed.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is None: + return + try: + item = [item for item in data.items if item['name'] == name][0] + except IndexError: + _LOGGER.error("Removing of item failed: %s cannot be found", name) + else: + data.async_update(item['id'], {'name': name, 'complete': True}) + data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() intent.async_register(hass, AddItemIntent()) intent.async_register(hass, ListTopItemsIntent()) + hass.services.async_register( + DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, + schema=SERVICE_ITEM_SCHEMA + ) + hass.http.register_view(ShoppingListView) hass.http.register_view(CreateShoppingListItemView) hass.http.register_view(UpdateShoppingListItemView) diff --git a/homeassistant/components/switch/hydrawise.py b/homeassistant/components/switch/hydrawise.py new file mode 100644 index 00000000000..d0abe5febf5 --- /dev/null +++ b/homeassistant/components/switch/hydrawise.py @@ -0,0 +1,103 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + ALLOWED_WATERING_TIME, CONF_WATERING_TIME, + DATA_HYDRAWISE, DEFAULT_WATERING_TIME, HydrawiseEntity, SWITCHES, + DEVICE_MAP, DEVICE_MAP_INDEX) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME): + vol.All(vol.In(ALLOWED_WATERING_TIME)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + default_watering_timer = config.get(CONF_WATERING_TIME) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + # create a switch for each zone + for zone in hydrawise.relays: + sensors.append( + HydrawiseSwitch(default_watering_timer, + zone, + sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSwitch(HydrawiseEntity, SwitchDevice): + """A switch implementation for Hydrawise device.""" + + def __init__(self, default_watering_timer, *args): + """Initialize a switch for Hydrawise device.""" + super().__init__(*args) + self._default_watering_timer = default_watering_timer + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + self._default_watering_timer, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 0, (self.data['relay']-1)) + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + 0, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 365, (self.data['relay']-1)) + + def update(self): + """Update device state.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise switch: %s", self._name) + if self._sensor_type == 'manual_watering': + if not mydata.running: + self._state = False + else: + self._state = int( + mydata.running[0]['relay']) == self.data['relay'] + elif self._sensor_type == 'auto_watering': + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay.get('suspended') is not None: + self._state = False + else: + self._state = True + break + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py index 8a5c4347cf7..a4bac8fee1c 100644 --- a/homeassistant/components/switch/raincloud.py +++ b/homeassistant/components/switch/raincloud.py @@ -88,7 +88,6 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'default_manual_timer': self._default_watering_timer, 'identifier': self.data.serial } diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index beb00eeca44..bdee64a3d54 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -4,12 +4,11 @@ This component provides support for RainMachine programs and zones. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.rainmachine/ """ - -from logging import getLogger +import logging from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, - RainMachineEntity) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, + PROGRAM_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -18,7 +17,7 @@ from homeassistant.helpers.dispatcher import ( DEPENDENCIES = ['rainmachine'] -_LOGGER = getLogger(__name__) +_LOGGER = logging.getLogger(__name__) ATTR_AREA = 'area' ATTR_CS_ON = 'cs_on' @@ -39,8 +38,6 @@ ATTR_SUN_EXPOSURE = 'sun_exposure' ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' -DEFAULT_ZONE_RUN = 60 * 10 - DAYS = [ 'Monday', 'Tuesday', @@ -86,7 +83,7 @@ SPRINKLER_TYPE_MAP = { 1: 'Popup Spray', 2: 'Rotors', 3: 'Surface Drip', - 4: 'Bubblers', + 4: 'Bubblers Drip', 99: 'Other' } @@ -99,14 +96,14 @@ SUN_EXPOSURE_MAP = { VEGETATION_MAP = { 0: 'Not Set', - 1: 'Not Set', - 2: 'Grass', + 2: 'Cool Season Grass', 3: 'Fruit Trees', 4: 'Flowers', 5: 'Vegetables', 6: 'Citrus', - 7: 'Bushes', - 8: 'Xeriscape', + 7: 'Trees and Bushes', + 9: 'Drought Tolerant Plants', + 10: 'Warm Season Grass', 99: 'Other' } @@ -141,26 +138,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineSwitch(RainMachineEntity, SwitchDevice): - """A class to represent a generic RainMachine entity.""" + """A class to represent a generic RainMachine switch.""" - def __init__(self, rainmachine, rainmachine_type, obj): - """Initialize a generic RainMachine entity.""" + def __init__(self, rainmachine, switch_type, obj): + """Initialize a generic RainMachine switch.""" + super().__init__(rainmachine) + + self._name = obj['name'] self._obj = obj - self._type = rainmachine_type + self._rainmachine_entity_id = obj['uid'] + self._switch_type = switch_type - super().__init__(rainmachine, rainmachine_type, obj.get('uid')) + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" return self._obj.get('active') + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace(':', ''), + self._switch_type, + self._rainmachine_entity_id) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" def __init__(self, rainmachine, obj): - """Initialize.""" + """Initialize a generic RainMachine switch.""" super().__init__(rainmachine, 'program', obj) @property @@ -168,11 +180,6 @@ class RainMachineProgram(RainMachineSwitch): """Return whether the program is running.""" return bool(self._obj.get('status')) - @property - def name(self) -> str: - """Return the name of the program.""" - return 'Program: {0}'.format(self._obj.get('name')) - @property def zones(self) -> list: """Return a list of active zones associated with this program.""" @@ -180,29 +187,29 @@ class RainMachineProgram(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.stop(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.start(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the program.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.programs.get( @@ -210,16 +217,11 @@ class RainMachineProgram(RainMachineSwitch): self._attrs.update({ ATTR_ID: self._obj['uid'], - ATTR_CS_ON: self._obj.get('cs_on'), - ATTR_CYCLES: self._obj.get('cycles'), - ATTR_DELAY: self._obj.get('delay'), - ATTR_DELAY_ON: self._obj.get('delay_on'), ATTR_SOAK: self._obj.get('soak'), - ATTR_STATUS: - PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) @@ -240,11 +242,6 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @property - def name(self) -> str: - """Return the name of the zone.""" - return 'Zone: {0}'.format(self._obj.get('name')) - @callback def _program_updated(self): """Update state, trigger updates.""" @@ -257,28 +254,28 @@ class RainMachineZone(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.start(self._rainmachine_entity_id, self._run_time) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the zone.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.zones.get( @@ -299,17 +296,19 @@ class RainMachineZone(RainMachineSwitch): self._properties_json.get( 'waterSense').get('precipitationRate'), ATTR_RESTRICTIONS: self._obj.get('restriction'), - ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SLOPE: SLOPE_TYPE_MAP.get( + self._properties_json.get('slope')), ATTR_SOIL_TYPE: - SOIL_TYPE_MAP[self._properties_json.get('sun')], + SOIL_TYPE_MAP.get(self._properties_json.get('sun')), ATTR_SPRINKLER_TYPE: - SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + SPRINKLER_TYPE_MAP.get( + self._properties_json.get('group_id')), ATTR_SUN_EXPOSURE: - SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + SUN_EXPOSURE_MAP.get(self._properties_json.get('sun')), ATTR_VEGETATION_TYPE: - VEGETATION_MAP[self._obj.get('type')], + VEGETATION_MAP.get(self._obj.get('type')), }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 5cc4de0d5ca..ebe92a2dcc2 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.42'] +REQUIREMENTS = ['pyvera==0.2.43'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 2cbf977443c..ae3a4e0be72 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.3'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ CONF_DISCOVERY_RETRY = 'discovery_retry' CONF_GATEWAYS = 'gateways' CONF_INTERFACE = 'interface' CONF_KEY = 'key' +CONF_DISABLE = 'disable' DOMAIN = 'xiaomi_aqara' @@ -73,6 +74,7 @@ GATEWAY_CONFIG = vol.Schema({ vol.All(cv.string, vol.Length(min=16, max=16)), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=9898): cv.port, + vol.Optional(CONF_DISABLE, default=False): cv.boolean, }) @@ -137,7 +139,8 @@ def setup(hass, config): xiaomi.listen() _LOGGER.debug("Gateways discovered. Listening for broadcasts") - for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover']: + for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover', + 'lock']: discovery.load_platform(hass, component, DOMAIN, {}, config) def stop_xiaomi(event): diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 3ea95ff1dd1..030e342847d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -18,7 +18,7 @@ from homeassistant.util import slugify REQUIREMENTS = [ 'bellows==0.6.0', 'zigpy==0.1.0', - 'zigpy-xbee==0.1.0', + 'zigpy-xbee==0.1.1', ] DOMAIN = 'zha' @@ -151,6 +151,11 @@ class ApplicationListener: # Wait for device_initialized, instead pass + def raw_device_initialized(self, device): + """Handle a device initialization without quirks loaded.""" + # Wait for device_initialized, instead + pass + def device_initialized(self, device): """Handle device joined and basic information discovered.""" self._hass.async_add_job(self.async_device_initialized(device, True)) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 1c083c3ca93..37c7f5592a0 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -48,6 +48,9 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', + zcl.clusters.smartenergy.Metering: 'sensor', + zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index d3628fd57f3..c33a16c632e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,27 +45,25 @@ PLATFORM_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Setup configured zones as well as home assistant zone if necessary.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {} + entities = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): - name = slugify(entry[CONF_NAME]) - if name not in zone_entries: + if slugify(entry[CONF_NAME]) not in zone_entries: zone = Zone(hass, entry[CONF_NAME], entry[CONF_LATITUDE], entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, entry[CONF_NAME], None, hass) + ENTITY_ID_FORMAT, entry[CONF_NAME], entities) hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][name] = zone + entities.add(zone.entity_id) - if HOME_ZONE not in hass.data[DOMAIN] and HOME_ZONE not in zone_entries: - name = hass.config.location_name - zone = Zone(hass, name, hass.config.latitude, hass.config.longitude, + if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: + zone = Zone(hass, hass.config.location_name, + hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][slugify(name)] = zone return True diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 1c0bb14f7e5..b62eeb67d32 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -68,8 +68,10 @@ def check_value_schema(value, schema): def node_name(node): """Return the name of the node.""" - return node.name or '{} {}'.format( - node.manufacturer_name, node.product_name) + if is_node_parsed(node): + return node.name or '{} {}'.format( + node.manufacturer_name, node.product_name) + return 'Unknown Node {}'.format(node.node_id) async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): @@ -89,4 +91,4 @@ async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): def is_node_parsed(node): """Check whether the node has been parsed or still waiting to be parsed.""" - return node.manufacturer_name and node.product_name + return bool((node.manufacturer_name and node.product_name) or node.name) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2f916e69b76..44bf542f7cd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,6 +548,31 @@ def _identify_config_schema(module): return '', schema +def _recursive_merge(pack_name, comp_name, config, conf, package): + """Merge package into conf, recursively.""" + for key, pack_conf in package.items(): + if isinstance(pack_conf, dict): + if not pack_conf: + continue + conf[key] = conf.get(key, OrderedDict()) + _recursive_merge(pack_name, comp_name, config, + conf=conf[key], package=pack_conf) + + elif isinstance(pack_conf, list): + if not pack_conf: + continue + conf[key] = cv.ensure_list(conf.get(key)) + conf[key].extend(cv.ensure_list(pack_conf)) + + else: + if conf.get(key) is not None: + _log_pkg_error( + pack_name, comp_name, config, + 'has keys that are defined multiple times') + else: + conf[key] = pack_conf + + def merge_packages_config(hass, config, packages, _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" @@ -607,11 +632,10 @@ def merge_packages_config(hass, config, packages, config[comp_name][key] = val continue - # The last merge type are sections that may occur only once + # The last merge type are sections that require recursive merging if comp_name in config: - _log_pkg_error( - pack_name, comp_name, config, "may occur only once" - " and it already exist in your main configuration") + _recursive_merge(pack_name, comp_name, config, + conf=config[comp_name], package=comp_conf) continue config[comp_name] = comp_conf diff --git a/homeassistant/const.py b/homeassistant/const.py index bb60a42fff9..552b6392595 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 70 -PATCH_VERSION = '1' +MINOR_VERSION = 71 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4a7df44ee5e..e76dc24d9dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,15 +1,15 @@ -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 11e337a76b5..e02305b5fbb 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.2.0', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==12.2.1', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 3bed3fa9fdf..f90f4d8c23b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,16 +1,16 @@ # Home Assistant core -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.3 +PyXiaomiGateway==0.9.4 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 @@ -61,7 +61,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.0 +TwitterAPI==2.5.4 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.5 @@ -161,7 +161,7 @@ blinkstick==1.1.8 # blinkt==0.1.0 # homeassistant.components.sensor.bitcoin -blockchain==1.4.0 +blockchain==1.4.4 # homeassistant.components.light.decora # bluepy==1.1.4 @@ -199,7 +199,7 @@ ciscosparkapi==0.4.2 coinbase==2.1.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.scripts.check_config colorlog==3.1.4 @@ -246,10 +246,10 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.6.1 +denonavr==0.7.2 # homeassistant.components.media_player.directv -directpy==0.2 +directpy==0.5 # homeassistant.components.sensor.discogs discogs_client==2.2.1 @@ -344,7 +344,7 @@ gTTS-token==1.1.1 gearbest_parser==1.0.5 # homeassistant.components.sensor.gitter -gitterpy==0.1.6 +gitterpy==0.1.7 # homeassistant.components.notify.gntp gntp==1.0.3 @@ -368,7 +368,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.3 +ha-philipsjs==0.0.4 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 @@ -389,13 +389,13 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180608.0b0 # homeassistant.components.homekit_controller # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.9.2.4 +homematicip==0.9.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -430,6 +430,9 @@ https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4 # homeassistant.components.media_player.lg_netcast https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 +# homeassistant.components.hydrawise +hydrawiser==0.1.1 + # homeassistant.components.sensor.bh1750 # homeassistant.components.sensor.bme280 # homeassistant.components.sensor.htu21d @@ -451,6 +454,9 @@ insteonlocal==0.53 # homeassistant.components.insteon_plm insteonplm==0.9.2 +# homeassistant.components.sensor.iperf3 +iperf3==0.1.10 + # homeassistant.components.verisure jsonpath==0.75 @@ -462,7 +468,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.0 +keyring==12.2.1 # homeassistant.scripts.keyring keyrings.alt==3.1 @@ -471,7 +477,7 @@ keyrings.alt==3.1 konnected==0.1.2 # homeassistant.components.eufy -lakeside==0.6 +lakeside==0.7 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http @@ -499,7 +505,7 @@ lightify==1.0.6.1 limitlessled==1.1.0 # homeassistant.components.linode -linode-api==4.1.4b2 +linode-api==4.1.9b1 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==2.0.2 @@ -509,10 +515,13 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.2 +locationsharinglib==2.0.7 # homeassistant.components.sensor.luftdaten -luftdaten==0.1.3 +luftdaten==0.2.0 + +# homeassistant.components.light.lw12wifi +lw12==0.9.2 # homeassistant.components.sensor.lyft lyft_rides==0.2 @@ -558,6 +567,9 @@ nad_receiver==0.0.9 # homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 +# homeassistant.components.sensor.netdata +netdata==0.1.2 + # homeassistant.components.discovery netdisco==1.4.1 @@ -642,7 +654,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.sensor.postnl -postnl_api==1.0.1 +postnl_api==1.0.2 # homeassistant.components.climate.proliphix proliphix==0.4.1 @@ -697,6 +709,9 @@ pyTibber==0.4.1 # homeassistant.components.switch.dlink pyW215==0.6.0 +# homeassistant.components.cover.ryobi_gdo +py_ryobi_gdo==0.0.10 + # homeassistant.components.ads pyads==2.2.6 @@ -716,7 +731,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.apple_tv -pyatv==0.3.9 +pyatv==0.3.10 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox @@ -759,6 +774,9 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 +# homeassistant.components.sensor.ebox +pyebox==1.1.4 + # homeassistant.components.climate.econet pyeconet==0.0.5 @@ -805,7 +823,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.42 +pyhomematic==0.1.43 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 @@ -916,7 +934,7 @@ pypollencom==1.1.2 pyqwikswitch==0.8 # homeassistant.components.rainbird -pyrainbird==0.1.3 +pyrainbird==0.1.6 # homeassistant.components.sabnzbd pysabnzbd==1.0.1 @@ -1015,7 +1033,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.2 # homeassistant.components.nest -python-nest==3.7.0 +python-nest==4.0.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 @@ -1057,7 +1075,7 @@ python-vlc==1.1.2 python-wink==1.7.3 # homeassistant.components.sensor.swiss_public_transport -python_opendata_transport==0.0.3 +python_opendata_transport==0.1.0 # homeassistant.components.zwave python_openzwave==0.4.3 @@ -1090,7 +1108,7 @@ pyupnp-async==0.1.0.2 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.42 +pyvera==0.2.43 # homeassistant.components.switch.vesync pyvesync==0.1.1 @@ -1129,10 +1147,10 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.1 +regenmaschine==0.4.2 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 @@ -1184,7 +1202,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.7.7 +shodan==1.8.1 # homeassistant.components.notify.simplepush simplepush==1.1.4 @@ -1225,7 +1243,7 @@ socialbladeclient==0.2 somecomfort==0.5.2 # homeassistant.components.sensor.speedtest -speedtest-cli==2.0.0 +speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 @@ -1233,7 +1251,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 @@ -1281,7 +1299,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.17 +total_connect_client==0.18 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission @@ -1379,7 +1397,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.09 +youtube_dl==2018.06.02 # homeassistant.components.light.zengge zengge==0.2 @@ -1391,7 +1409,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.1.0 +zigpy-xbee==0.1.1 # homeassistant.components.zha zigpy==0.1.0 diff --git a/requirements_docs.txt b/requirements_docs.txt index 5ef38e1537e..0556b35fc08 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.4 +Sphinx==1.7.5 sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e04d3fdb03..9c3486d104e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.html5 PyJWT==1.6.0 @@ -44,7 +44,7 @@ apns2==0.3.0 caldav==0.5.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.components.device_tracker.upc_connect defusedxml==0.5.0 @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180608.0b0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -168,7 +168,7 @@ pyupnp-async==0.1.0.2 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 diff --git a/setup.py b/setup.py index 2469f32d77e..4390b980f9e 100755 --- a/setup.py +++ b/setup.py @@ -42,18 +42,18 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'requests==2.18.4', - 'pyyaml>=3.11,<4', - 'pytz>=2018.04', - 'pip>=8.0.3', - 'jinja2>=2.10', - 'voluptuous==0.11.1', - 'typing>=3,<4', - 'aiohttp==3.1.3', - 'async_timeout==2.0.1', + 'aiohttp==3.2.1', 'astral==1.6.1', - 'certifi>=2018.04.16', + 'async_timeout==3.0.0', 'attrs==18.1.0', + 'certifi>=2018.04.16', + 'jinja2>=2.10', + 'pip>=8.0.3', + 'pytz>=2018.04', + 'pyyaml>=3.11,<4', + 'requests==2.18.4', + 'typing>=3,<4', + 'voluptuous==0.11.1', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7a8c097a730..33f1a7aa704 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -207,6 +207,7 @@ class TestAutomation(unittest.TestCase): """Test triggers.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { + 'alias': 'test', 'trigger': [ { 'platform': 'event', @@ -228,7 +229,9 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 0 - self.hass.services.call('automation', 'trigger', blocking=True) + self.hass.services.call('automation', 'trigger', + {'entity_id': 'automation.test'}, + blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 88dd0dae737..2e33e28fa57 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -26,7 +26,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -41,7 +41,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup( config_entry, 'binary_sensor') # To flush out the service call to update the group @@ -77,3 +78,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 81b1e315085..1b580d0eb9b 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -210,7 +210,7 @@ def test_cloud_connect_invalid_auth(mock_client, caplog, mock_cloud): """Test invalid auth detected by server.""" conn = iot.CloudIoT(mock_cloud) mock_client.receive.side_effect = \ - client_exceptions.WSServerHandshakeError(None, None, code=401) + client_exceptions.WSServerHandshakeError(None, None, status=401) yield from conn.connect() diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d86475b35ef..df3310f3d6f 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -21,7 +21,9 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass await flow.async_step_init() - result = await flow.async_step_link(user_input={}) + await flow.async_step_link(user_input={}) + result = await flow.async_step_options( + user_input={'allow_clip_sensor': True}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -29,7 +31,8 @@ async def test_flow_works(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -146,14 +149,14 @@ async def test_bridge_discovery_config_file(hass): 'port': 80, 'serial': 'id' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -214,12 +217,34 @@ async def test_import_with_api_key(hass): 'port': 80, 'api_key': '1234567890ABCDEF' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True + } + + +async def test_options(hass, aioclient_mock): + """Test that options work and that bridgeid can be requested.""" + aioclient_mock.get('http://1.2.3.4:80/api/1234567890ABCDEF/config', + json={"bridgeid": "id"}) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF'} + result = await flow.async_step_options( + user_input={'allow_clip_sensor': False}) + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': False } diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 888094deea6..1cee08feb0a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -172,3 +172,21 @@ async def test_add_new_remote(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, + 'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False} + remote = Mock() + remote.name = 'name' + remote.type = 'CLIPSwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e17419e7fd5..f67a6cbccec 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -9,6 +9,12 @@ from homeassistant.components.hassio import async_check_config from tests.common import mock_coro +MOCK_ENVIRON = { + 'HASSIO': '127.0.0.1', + 'HASSIO_TOKEN': 'abcdefgh', +} + + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" @@ -18,7 +24,7 @@ def test_setup_api_ping(hass, aioclient_mock): "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', {}) assert result @@ -38,7 +44,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -66,7 +72,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -95,7 +101,7 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': {}, 'hassio': {} @@ -119,7 +125,7 @@ def test_setup_core_push_timezone(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, 'homeassistant': { @@ -143,7 +149,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, @@ -165,7 +171,7 @@ def test_fail_setup_without_environ_var(hass): @asyncio.coroutine def test_fail_setup_cannot_connect(hass): """Fail setup if cannot connect.""" - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch('homeassistant.components.hassio.HassIO.is_connected', Mock(return_value=mock_coro(None))): result = yield from async_setup_component(hass, 'hassio', {}) diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py new file mode 100644 index 00000000000..f7839265939 --- /dev/null +++ b/tests/components/homekit/conftest.py @@ -0,0 +1,16 @@ +"""HomeKit session fixtures.""" +from unittest.mock import patch + +import pytest + +from pyhap.accessory_driver import AccessoryDriver + + +@pytest.fixture(scope='session') +def hk_driver(): + """Return a custom AccessoryDriver instance for HomeKit accessory init.""" + with patch('pyhap.accessory_driver.Zeroconf'), \ + patch('pyhap.accessory_driver.AccessoryEncoder'), \ + patch('pyhap.accessory_driver.HAPServer'), \ + patch('pyhap.accessory_driver.AccessoryDriver.publish'): + return AccessoryDriver(pincode=b'123-45-678') diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 3d1c335f8ae..711c38443f2 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -26,7 +26,7 @@ async def test_debounce(hass): arguments = None counter = 0 - mock = Mock(hass=hass) + mock = Mock(hass=hass, debounce={}) debounce_demo = debounce(demo_func) assert debounce_demo.__name__ == 'demo_func' @@ -50,13 +50,14 @@ async def test_debounce(hass): assert counter == 2 -async def test_home_accessory(hass): +async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = 'homekit.accessory' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, 'Home Accessory', + entity_id, 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' assert acc.aid == 2 @@ -75,6 +76,7 @@ async def test_home_accessory(hass): with patch('homeassistant.components.homekit.accessories.' 'HomeAccessory.update_state') as mock_update_state: await hass.async_add_job(acc.run) + await hass.async_block_till_done() state = hass.states.get(entity_id) mock_update_state.assert_called_with(state) @@ -86,14 +88,15 @@ async def test_home_accessory(hass): acc.update_state('new_state') # Test model name from domain - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + entity_id = 'test_model.demo' + acc = HomeAccessory('hass', hk_driver, 'test_name', entity_id, 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -def test_home_bridge(): +def test_home_bridge(hk_driver): """Test HomeBridge class.""" - bridge = HomeBridge('hass') + bridge = HomeBridge('hass', hk_driver) assert bridge.hass == 'hass' assert bridge.display_name == BRIDGE_NAME assert bridge.category == 2 # Category.BRIDGE @@ -107,7 +110,7 @@ def test_home_bridge(): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ BRIDGE_SERIAL_NUMBER - bridge = HomeBridge('hass', 'test_name') + bridge = HomeBridge('hass', hk_driver, 'test_name') assert bridge.display_name == 'test_name' assert len(bridge.services) == 1 serv = bridge.services[0] # SERV_ACCESSORY_INFO @@ -118,7 +121,6 @@ def test_home_bridge(): def test_home_driver(): """Test HomeDriver class.""" - bridge = HomeBridge('hass') ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' @@ -126,9 +128,11 @@ def test_home_driver(): with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - driver = HomeDriver('hass', bridge, ip_address, port, path) + driver = HomeDriver('hass', address=ip_address, port=port, + persist_file=path) - mock_driver.assert_called_with(bridge, ip_address, port, path) + mock_driver.assert_called_with(address=ip_address, port=port, + persist_file=path) driver.state = Mock(pincode=pin) # pair diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 25a0dd3f1cb..4de68057084 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,26 +4,43 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN -from homeassistant.components.climate import ( - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +import homeassistant.components.cover as cover +import homeassistant.components.climate as climate +import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES +from homeassistant.components.homekit.const import ( + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, + TEMP_FAHRENHEIT) def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" # not supported entity - assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + assert get_accessory(None, None, State('demo.demo', 'on'), 2, {}) \ + is None # invalid aid - assert get_accessory(None, State('light.demo', 'on'), None, None) is None + assert get_accessory(None, None, State('light.demo', 'on'), None, None) \ + is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg +def test_not_supported_media_player(): + """Test if mode isn't supported and if no supported modes.""" + # selected mode for entity not supported + config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}} + entity_state = State('media_player.demo', 'on') + get_accessory(None, None, entity_state, 2, config) is None + + # no supported modes for entity + entity_state = State('media_player.demo', 'on') + assert get_accessory(None, None, entity_state, 2, {}) is None + + @pytest.mark.parametrize('config, name', [ ({CONF_NAME: 'Customize Name'}, 'Customize Name'), ]) @@ -32,27 +49,32 @@ def test_customize_options(config, name): mock_type = Mock() with patch.dict(TYPES, {'Light': mock_type}): entity_state = State('light.demo', 'on') - get_accessory(None, entity_state, 2, config) - mock_type.assert_called_with(None, name, 'light.demo', 2, config) + get_accessory(None, None, entity_state, 2, config) + mock_type.assert_called_with(None, None, name, + 'light.demo', 2, config) @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), + ('MediaPlayer', 'media_player.test', 'on', + {ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: None}}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', - {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), + {ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_LOW | + climate.SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, config) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called if config: @@ -62,7 +84,7 @@ def test_types(type_name, entity_id, state, attrs, config): @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ ('GarageDoorOpener', 'cover.garage_door', 'open', {ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE}), ('WindowCovering', 'cover.set_position', 'open', {ATTR_SUPPORTED_FEATURES: 4}), ('WindowCoveringBasic', 'cover.open_window', 'open', @@ -73,7 +95,7 @@ def test_type_covers(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called @@ -104,19 +126,23 @@ def test_type_sensors(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called -@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ - ('Switch', 'switch.test', 'on', {}), - ('Switch', 'remote.test', 'on', {}), - ('Switch', 'input_boolean.test', 'on', {}), +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Outlet', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_OUTLET}), + ('Switch', 'automation.test', 'on', {}, {}), + ('Switch', 'input_boolean.test', 'on', {}, {}), + ('Switch', 'remote.test', 'on', {}, {}), + ('Switch', 'script.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), ]) -def test_type_switches(type_name, entity_id, state, attrs): +def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 31337088b33..08e8da7857e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -94,11 +94,12 @@ async def test_setup_auto_start_disabled(hass): assert homekit.start.called is False -async def test_homekit_setup(hass): +async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_job(homekit.setup) @@ -106,41 +107,42 @@ async def test_homekit_setup(hass): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, homekit.bridge, port=DEFAULT_PORT, - address=IP_ADDRESS, persist_file=path) + hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path) # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 -async def test_homekit_setup_ip_address(hass): +async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) + hass, address='172.0.0.0', port=DEFAULT_PORT, persist_file=ANY) async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.driver = 'driver' homekit.bridge = mock_bridge = Mock() with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 363398124, {}) assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 294192020, {}) assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 429982757, {}) mock_bridge.add_accessory.assert_called_with('acc') @@ -164,30 +166,35 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_start(hass, debounce_patcher): +async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = Mock() - homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = 'bridge' + homekit.driver = hk_driver hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ mock_add_acc, \ - patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg, \ + patch('pyhap.accessory_driver.AccessoryDriver.add_accessory') as \ + hk_driver_add_acc, \ + patch('pyhap.accessory_driver.AccessoryDriver.start') as \ + hk_driver_start: await hass.async_add_job(homekit.start) mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert mock_driver.start.called is True + hk_driver_add_acc.assert_called_with('bridge') + assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started - mock_driver.reset_mock() + hk_driver_start.reset_mock() await hass.async_add_job(homekit.start) - assert mock_driver.start.called is False + assert not hk_driver_start.called async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8138d1c506b..c69ddacd328 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -28,13 +28,13 @@ def cls(): patcher.stop() -async def test_garage_door_open_close(hass, cls): +async def test_garage_door_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) + acc = cls.garage(hass, hk_driver, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -85,13 +85,13 @@ async def test_garage_door_open_close(hass, cls): assert acc.char_target_state.value == 0 -async def test_window_set_cover_position(hass, cls): +async def test_window_set_cover_position(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.window(hass, 'Cover', entity_id, 2, None) + acc = cls.window(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -133,13 +133,13 @@ async def test_window_set_cover_position(hass, cls): assert acc.char_target_position.value == 75 -async def test_window_open_close(hass, cls): +async def test_window_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -196,13 +196,13 @@ async def test_window_open_close(hass, cls): assert acc.char_position_state.value == 2 -async def test_window_open_close_stop(hass, cls): +async def test_window_open_close_stop(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) # Set from HomeKit diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index f96fe19d603..ba7d4ccdcf0 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -27,14 +27,14 @@ def cls(): patcher.stop() -async def test_fan_basic(hass, cls): +async def test_fan_basic(hass, hk_driver, cls): """Test fan with char state.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 3 # Fan @@ -75,7 +75,7 @@ async def test_fan_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_fan_direction(hass, cls): +async def test_fan_direction(hass, hk_driver, cls): """Test fan with direction.""" entity_id = 'fan.demo' @@ -83,7 +83,7 @@ async def test_fan_direction(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_direction.value == 0 @@ -113,14 +113,14 @@ async def test_fan_direction(hass, cls): assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE -async def test_fan_oscillate(hass, cls): +async def test_fan_oscillate(hass, hk_driver, cls): """Test fan with oscillate.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_swing.value == 0 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 7a1db7b3f71..a9a5f1c3ece 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -26,14 +26,14 @@ def cls(): patcher.stop() -async def test_light_basic(hass, cls): +async def test_light_basic(hass, hk_driver, cls): """Test light with char state.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 5 # Lightbulb @@ -74,14 +74,14 @@ async def test_light_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_brightness(hass, cls): +async def test_light_brightness(hass, hk_driver, cls): """Test light with brightness.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_brightness.value == 0 @@ -118,7 +118,7 @@ async def test_light_brightness(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_color_temperature(hass, cls): +async def test_light_color_temperature(hass, hk_driver, cls): """Test light with color temperature.""" entity_id = 'light.demo' @@ -126,7 +126,7 @@ async def test_light_color_temperature(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_color_temperature.value == 153 @@ -145,7 +145,7 @@ async def test_light_color_temperature(hass, cls): assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 -async def test_light_rgb_color(hass, cls): +async def test_light_rgb_color(hass, hk_driver, cls): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -153,7 +153,7 @@ async def test_light_rgb_color(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index f4698b1380b..8f18a591019 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -9,7 +9,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_lock_unlock(hass): +async def test_lock_unlock(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -17,7 +17,7 @@ async def test_lock_unlock(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -66,13 +66,13 @@ async def test_lock_unlock(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_code(hass, config): +async def test_no_code(hass, hk_driver, config): """Test accessory if lock doesn't require a code.""" entity_id = 'lock.kitchen_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, 'lock') diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py new file mode 100644 index 00000000000..4076b1f8a89 --- /dev/null +++ b/tests/components/homekit/test_type_media_players.py @@ -0,0 +1,117 @@ +"""Test different accessory types: Media Players.""" + +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) +from homeassistant.components.homekit.type_media_players import MediaPlayer +from homeassistant.components.homekit.const import ( + CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) + +from tests.common import async_mock_service + + +async def test_media_player_set_state(hass, hk_driver): + """Test if accessory and HA are updated accordingly.""" + config = {CONF_FEATURE_LIST: { + FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None}} + entity_id = 'media_player.test' + + hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, + ATTR_MEDIA_VOLUME_MUTED: False}) + await hass.async_block_till_done() + acc = MediaPlayer(hass, hk_driver, 'MediaPlayer', entity_id, 2, config) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 8 # Switch + + assert acc.chars[FEATURE_ON_OFF].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value == 0 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 0 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) + await hass.async_block_till_done() + assert acc.chars[FEATURE_ON_OFF].value == 1 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.chars[FEATURE_ON_OFF].value == 0 + + hass.states.async_set(entity_id, STATE_PLAYING) + await hass.async_block_till_done() + assert acc.chars[FEATURE_PLAY_PAUSE].value == 1 + assert acc.chars[FEATURE_PLAY_STOP].value == 1 + + hass.states.async_set(entity_id, STATE_PAUSED) + await hass.async_block_till_done() + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 + + hass.states.async_set(entity_id, STATE_IDLE) + await hass.async_block_till_done() + assert acc.chars[FEATURE_PLAY_STOP].value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + call_media_play = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PLAY) + call_media_pause = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE) + call_media_stop = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_STOP) + call_toggle_mute = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) + + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, False) + await hass.async_block_till_done() + assert call_media_pause + assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, False) + await hass.async_block_till_done() + assert call_media_stop + assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, True) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True + + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, False) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 7b72404cdaa..3ddce0f36eb 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_switch_set_state(hass): +async def test_switch_set_state(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -20,7 +20,8 @@ async def test_switch_set_state(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -95,13 +96,14 @@ async def test_switch_set_state(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_alarm_code(hass, config): +async def test_no_alarm_code(hass, hk_driver, config): """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index e36ae67da12..54ecbcb196f 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -8,13 +8,14 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -async def test_temperature(hass): +async def test_temperature(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) + acc = TemperatureSensor(hass, hk_driver, 'Temperature', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -40,13 +41,13 @@ async def test_temperature(hass): assert acc.char_temp.value == 24 -async def test_humidity(hass): +async def test_humidity(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) + acc = HumiditySensor(hass, hk_driver, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -63,13 +64,14 @@ async def test_humidity(hass): assert acc.char_humidity.value == 20 -async def test_air_quality(hass): +async def test_air_quality(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) + acc = AirQualitySensor(hass, hk_driver, 'Air Quality', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -94,13 +96,13 @@ async def test_air_quality(hass): assert acc.char_quality.value == 5 -async def test_co2(hass): +async def test_co2(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) + acc = CarbonDioxideSensor(hass, hk_driver, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -129,13 +131,13 @@ async def test_co2(hass): assert acc.char_detected.value == 0 -async def test_light(hass): +async def test_light(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = LightSensor(hass, 'Light', entity_id, 2, None) + acc = LightSensor(hass, hk_driver, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -152,7 +154,7 @@ async def test_light(hass): assert acc.char_light.value == 300 -async def test_binary(hass): +async def test_binary(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'binary_sensor.opening' @@ -160,7 +162,7 @@ async def test_binary(hass): {ATTR_DEVICE_CLASS: 'opening'}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Window Opening', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -193,7 +195,7 @@ async def test_binary(hass): assert acc.char_detected.value == 0 -async def test_binary_device_classes(hass): +async def test_binary_device_classes(hass, hk_driver): """Test if services and characteristics are assigned correctly.""" entity_id = 'binary_sensor.demo' @@ -202,6 +204,7 @@ async def test_binary_device_classes(hass): {ATTR_DEVICE_CLASS: device_class}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Binary Sensor', + entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 5fc0b6ce1b9..3a09d2715d1 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,22 +2,67 @@ import pytest from homeassistant.core import split_entity_id -from homeassistant.components.homekit.type_switches import Switch +from homeassistant.components.homekit.type_switches import Outlet, Switch from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service +async def test_outlet_set_state(hass, hk_driver): + """Test if Outlet accessory and HA are updated accordingly.""" + entity_id = 'switch.outlet_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Outlet(hass, hk_driver, 'Outlet', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 7 # Outlet + + assert acc.char_on.value is False + assert acc.char_outlet_in_use.value is True + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + @pytest.mark.parametrize('entity_id', [ - 'switch.test', 'remote.test', 'input_boolean.test']) -async def test_switch_set_state(hass, entity_id): + 'automation.test', + 'input_boolean.test', + 'remote.test', + 'script.test', + 'switch.test', +]) +async def test_switch_set_state(hass, hk_driver, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Switch(hass, 'Switch', entity_id, 2, None) + acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 8 # Switch diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 337ad23ad05..00e3e2d22fc 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,12 +1,16 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple +from unittest.mock import patch import pytest from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, + STATE_AUTO, STATE_COOL, STATE_HEAT) +from homeassistant.components.homekit.const import ( + PROP_MAX_VALUE, PROP_MIN_VALUE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -27,14 +31,15 @@ def cls(): patcher.stop() -async def test_default_thermostat(hass, cls): +async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' - hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 9 # Thermostat @@ -47,6 +52,9 @@ async def test_default_thermostat(hass, cls): assert acc.char_cooling_thresh_temp is None assert acc.char_heating_thresh_temp is None + assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_HEAT, {ATTR_OPERATION_MODE: STATE_HEAT, ATTR_TEMPERATURE: 22.0, @@ -166,19 +174,29 @@ async def test_default_thermostat(hass, cls): assert acc.char_target_heat_cool.value == 1 -async def test_auto_thermostat(hass, cls): +async def test_auto_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, ATTR_TARGET_TEMP_HIGH: 22.0, @@ -241,7 +259,7 @@ async def test_auto_thermostat(hass, cls): assert acc.char_cooling_thresh_temp.value == 25.0 -async def test_power_state(hass, cls): +async def test_power_state(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -252,8 +270,9 @@ async def test_power_state(hass, cls): ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.support_power_state is True assert acc.char_current_heat_cool.value == 1 @@ -297,15 +316,18 @@ async def test_power_state(hass, cls): assert acc.char_target_heat_cool.value == 0 -async def test_thermostat_fahrenheit(hass, cls): +async def test_thermostat_fahrenheit(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + with patch.object(hass.config.units, 'temperature_unit', + new=TEMP_FAHRENHEIT): + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, @@ -345,3 +367,23 @@ async def test_thermostat_fahrenheit(hass, cls): assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 + + +async def test_get_temperature_range(hass, hk_driver, cls): + """Test if temperature range is evaluated correctly.""" + entity_id = 'climate.test' + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (20, 25) + + acc._unit = TEMP_FAHRENHEIT + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (15.6, 21.1) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0755e8f54d4..fa9fddee5fc 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,16 +2,21 @@ import pytest import voluptuous as vol -from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.core import State +from homeassistant.components.homekit.const import ( + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, TYPE_OUTLET) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, - show_setup_message, temperature_to_homekit, temperature_to_states) + show_setup_message, temperature_to_homekit, temperature_to_states, + validate_media_player_features) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, STATE_UNKNOWN, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -20,7 +25,13 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: 'invalid_feature'}]}}, + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_ON_OFF}]}}, + {'switch.test': {CONF_TYPE: 'invalid_type'}}] for conf in configs: with pytest.raises(vol.Invalid): @@ -39,6 +50,30 @@ def test_validate_entity_config(): assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ {'lock.demo': {ATTR_CODE: '1234'}} + assert vec({'media_player.demo': {}}) == \ + {'media_player.demo': {CONF_FEATURE_LIST: {}}} + config = {CONF_FEATURE_LIST: [{CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_PLAY_PAUSE}]} + assert vec({'media_player.demo': config}) == \ + {'media_player.demo': {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} + + +def test_validate_media_player_features(): + """Test validate modes for media players.""" + config = {} + attrs = {ATTR_SUPPORTED_FEATURES: 20873} + entity_state = State('media_player.demo', 'on', attrs) + assert validate_media_player_features(entity_state, config) is True + + config = {FEATURE_ON_OFF: None} + assert validate_media_player_features(entity_state, config) is True + + entity_state = State('media_player.demo', 'on') + assert validate_media_player_features(entity_state, config) is False + def test_convert_to_float(): """Test convert_to_float method.""" diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py index 06a7089e052..5e5a829662a 100644 --- a/tests/components/sensor/test_bom.py +++ b/tests/components/sensor/test_bom.py @@ -1,16 +1,16 @@ """The tests for the BOM Weather sensor platform.""" +import json import re import unittest -import json -import requests from unittest.mock import patch from urllib.parse import urlparse -from homeassistant.setup import setup_component -from homeassistant.components import sensor - +import requests from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + assert_setup_component, get_test_home_assistant, load_fixture) + +from homeassistant.components import sensor +from homeassistant.setup import setup_component VALID_CONFIG = { 'platform': 'bom', @@ -89,9 +89,11 @@ class TestBOMWeatherSensor(unittest.TestCase): self.assertTrue(setup_component( self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) - self.assertEqual('Fine', self.hass.states.get( - 'sensor.bom_fake_weather').state) - self.assertEqual('1021.7', self.hass.states.get( - 'sensor.bom_fake_pressure_mb').state) - self.assertEqual('25.0', self.hass.states.get( - 'sensor.bom_fake_feels_like_c').state) + weather = self.hass.states.get('sensor.bom_fake_weather').state + self.assertEqual('Fine', weather) + + pressure = self.hass.states.get('sensor.bom_fake_pressure_mb').state + self.assertEqual('1021.7', pressure) + + feels_like = self.hass.states.get('sensor.bom_fake_feels_like_c').state + self.assertEqual('25.0', feels_like) diff --git a/tests/components/sensor/test_coinmarketcap.py b/tests/components/sensor/test_coinmarketcap.py index 15c254bfb27..37a63e5cba5 100644 --- a/tests/components/sensor/test_coinmarketcap.py +++ b/tests/components/sensor/test_coinmarketcap.py @@ -11,8 +11,9 @@ from tests.common import ( VALID_CONFIG = { 'platform': 'coinmarketcap', - 'currency': 'ethereum', + 'currency_id': 1027, 'display_currency': 'EUR', + 'display_currency_decimals': 3 } @@ -39,6 +40,6 @@ class TestCoinMarketCapSensor(unittest.TestCase): state = self.hass.states.get('sensor.ethereum') assert state is not None - assert state.state == '240.47' + assert state.state == '493.455' assert state.attributes.get('symbol') == 'ETH' assert state.attributes.get('unit_of_measurement') == 'EUR' diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index 8f6a53e6e65..d7cdb458646 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -41,7 +41,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -57,7 +57,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') # To flush out the service call to update the group await hass.async_block_till_done() @@ -97,3 +98,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clipsensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPTemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index d9c29cdae83..6a1d5a55c47 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -89,7 +89,7 @@ async def test_register_before_setup(hass): assert intent.text_input == 'I would like the Grolsch beer' -async def test_http_processing_intent(hass, test_client): +async def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): """Test Intent Handler.""" @@ -119,7 +119,7 @@ async def test_http_processing_intent(hass, test_client): }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -243,7 +243,7 @@ async def test_toggle_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api(hass, test_client): +async def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -251,7 +251,7 @@ async def test_http_api(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -267,7 +267,7 @@ async def test_http_api(hass, test_client): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api_wrong_data(hass, test_client): +async def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -275,7 +275,7 @@ async def test_http_api_wrong_data(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 123 diff --git a/tests/components/test_panel_custom.py b/tests/components/test_panel_custom.py index d33221da2a7..596aa1b3c0b 100644 --- a/tests/components/test_panel_custom.py +++ b/tests/components/test_panel_custom.py @@ -1,23 +1,11 @@ """The tests for the panel_custom component.""" -import asyncio from unittest.mock import Mock, patch -import pytest - from homeassistant import setup from homeassistant.components import frontend -from tests.common import mock_component - -@pytest.fixture(autouse=True) -def mock_frontend_loaded(hass): - """Mock frontend is loaded.""" - mock_component(hass, 'frontend') - - -@asyncio.coroutine -def test_webcomponent_custom_path_not_found(hass): +async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' @@ -33,45 +21,96 @@ def test_webcomponent_custom_path_not_found(hass): } with patch('os.path.isfile', Mock(return_value=False)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert not result assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0 -@asyncio.coroutine -def test_webcomponent_custom_path(hass): +async def test_webcomponent_custom_path(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' config = { 'panel_custom': { - 'name': 'todomvc', + 'name': 'todo-mvc', 'webcomponent_path': filename, 'sidebar_title': 'Sidebar Title', 'sidebar_icon': 'mdi:iconicon', 'url_path': 'nice_url', - 'config': 5, + 'config': { + 'hello': 'world', + } } } with patch('os.path.isfile', Mock(return_value=True)): with patch('os.access', Mock(return_value=True)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert result panels = hass.data.get(frontend.DATA_PANELS, []) - assert len(panels) == 1 + assert panels assert 'nice_url' in panels panel = panels['nice_url'] - assert panel.config == 5 + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'html_url': '/api/panel_custom/todo-mvc', + 'name': 'todo-mvc', + 'embed_iframe': False, + 'trust_external': False, + }, + } assert panel.frontend_url_path == 'nice_url' assert panel.sidebar_icon == 'mdi:iconicon' assert panel.sidebar_title == 'Sidebar Title' - assert panel.path == filename + + +async def test_js_webcomponent(hass): + """Test if a web component is found in config panels dir.""" + config = { + 'panel_custom': { + 'name': 'todo-mvc', + 'js_url': '/local/bla.js', + 'sidebar_title': 'Sidebar Title', + 'sidebar_icon': 'mdi:iconicon', + 'url_path': 'nice_url', + 'config': { + 'hello': 'world', + }, + 'embed_iframe': True, + 'trust_external_script': True, + } + } + + result = await setup.async_setup_component( + hass, 'panel_custom', config + ) + assert result + + panels = hass.data.get(frontend.DATA_PANELS, []) + + assert panels + assert 'nice_url' in panels + + panel = panels['nice_url'] + + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'js_url': '/local/bla.js', + 'name': 'todo-mvc', + 'embed_iframe': True, + 'trust_external': True, + } + } + assert panel.frontend_url_path == 'nice_url' + assert panel.sidebar_icon == 'mdi:iconicon' + assert panel.sidebar_title == 'Sidebar Title' diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 1c698438f2c..c26b3375f3a 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -59,7 +59,6 @@ class TestComponentZone(unittest.TestCase): assert self.hass.config.latitude == state.attributes['latitude'] assert self.hass.config.longitude == state.attributes['longitude'] assert not state.attributes.get('passive', False) - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup(self): """Test a successful setup.""" @@ -79,8 +78,6 @@ class TestComponentZone(unittest.TestCase): assert info['longitude'] == state.attributes['longitude'] assert info['radius'] == state.attributes['radius'] assert info['passive'] == state.attributes['passive'] - assert 'test_zone' in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup_zone_skips_home_zone(self): """Test that zone named Home should override hass home zone.""" @@ -94,8 +91,17 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert info['name'] == state.name - assert 'home' in self.hass.data[zone.DOMAIN] - assert 'test_home' not in self.hass.data[zone.DOMAIN] + + def test_setup_name_can_be_same_on_multiple_zones(self): + """Test that zone named Home should override hass home zone.""" + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component( + self.hass, zone.DOMAIN, {'zone': [info, info]}) + assert len(self.hass.states.entity_ids('zone')) == 3 def test_setup_registered_zone_skips_home_zone(self): """Test that config entry named home should override hass home zone.""" @@ -105,7 +111,6 @@ class TestComponentZone(unittest.TestCase): entry.add_to_hass(self.hass) assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 0 - assert not self.hass.data[zone.DOMAIN] def test_setup_registered_zone_skips_configured_zone(self): """Test if config entry will override configured zone.""" @@ -123,8 +128,6 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.test_zone') assert not state - assert 'test_zone' not in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_active_zone_skips_passive_zones(self): """Test active and passive zones.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index a25b725e500..e608dcccaba 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -238,7 +238,8 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) + node = MockNode( + node_id=14, manufacturer_name=None, name=None, is_ready=False) sleeps = [] @@ -263,7 +264,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_logger.warning.mock_calls) == 1 assert mock_logger.warning.mock_calls[0][1][1:] == \ (14, const.NODE_READY_WAIT_SECS) - assert hass.states.get('zwave.mock_node').state is 'unknown' + assert hass.states.get('zwave.unknown_node_14').state is 'unknown' @asyncio.coroutine diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index f4d9b3ef0e8..b91245d5a12 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -363,6 +363,7 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_unique_id_missing_data(self): """Test unique_id.""" self.node.manufacturer_name = None + self.node.name = None entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) self.assertIsNone(entity.unique_id) diff --git a/tests/fixtures/coinmarketcap.json b/tests/fixtures/coinmarketcap.json index 20f5e4fe91e..5a6b63c5da1 100644 --- a/tests/fixtures/coinmarketcap.json +++ b/tests/fixtures/coinmarketcap.json @@ -1,21 +1,36 @@ -[ - { - "id": "ethereum", - "name": "Ethereum", - "symbol": "ETH", - "rank": "2", - "price_usd": "282.423", - "price_btc": "0.048844", - "24h_volume_usd": "407024000.0", - "market_cap_usd": "26908205315.0", - "available_supply": "95276253.0", - "total_supply": "95276253.0", - "percent_change_1h": "0.06", - "percent_change_24h": "-4.57", - "percent_change_7d": "-16.39", - "last_updated": "1508776751", - "price_eur": "240.473299695", - "24h_volume_eur": "346566690.16", - "market_cap_eur": "22911395039.0" +{ + "cached": false, + "data": { + "id": 1027, + "name": "Ethereum", + "symbol": "ETH", + "website_slug": "ethereum", + "rank": 2, + "circulating_supply": 99619842.0, + "total_supply": 99619842.0, + "max_supply": null, + "quotes": { + "USD": { + "price": 577.019, + "volume_24h": 2839960000.0, + "market_cap": 57482541899.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + }, + "EUR": { + "price": 493.454724572, + "volume_24h": 2428699712.48, + "market_cap": 49158380042.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + } + }, + "last_updated": 1527098658 + }, + "metadata": { + "timestamp": 1527098716, + "error": null } -] \ No newline at end of file +} \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 4b1115c3814..d22d6b2acfd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -654,21 +654,81 @@ def test_merge_type_mismatch(merge_log_err, hass): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err, hass): - """Test if we have a merge for a comp that may occur only once.""" - packages = { - 'pack_2': { - 'mqtt': {}, - 'api': {}, # No config schema - }, - } +def test_merge_once_only_keys(merge_log_err, hass): + """Test if we have a merge for a comp that may occur only once. Keys.""" + packages = {'pack_2': {'api': { + 'key_3': 3, + }}} config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, - 'mqtt': {}, 'api': {} + 'api': { + 'key_1': 1, + 'key_2': 2, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == {'key_1': 1, 'key_2': 2, 'key_3': 3, } + + # Duplicate keys error + packages = {'pack_2': {'api': { + 'key': 2, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': {'key': 1, } } config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 - assert len(config) == 3 + + +def test_merge_once_only_lists(hass): + """Test if we have a merge for a comp that may occur only once. Lists.""" + packages = {'pack_2': {'api': { + 'list_1': ['item_2', 'item_3'], + 'list_2': ['item_1'], + 'list_3': [], + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'list_1': ['item_1'], + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'list_1': ['item_1', 'item_2', 'item_3'], + 'list_2': ['item_1'], + } + + +def test_merge_once_only_dictionaries(hass): + """Test if we have a merge for a comp that may occur only once. Dicts.""" + packages = {'pack_2': {'api': { + 'dict_1': { + 'key_2': 2, + 'dict_1.1': {'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + 'dict_3': {}, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'dict_1': { + 'key_1': 1, + 'dict_1.1': {'key_1.1': 1.1, } + }, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'dict_1': { + 'key_1': 1, + 'key_2': 2, + 'dict_1.1': {'key_1.1': 1.1, 'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + } def test_merge_id_schema(hass): diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 06676140702..d0599c2e74c 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg deleted file mode 100755 index 914c2648e56..00000000000 --- a/virtualization/Docker/scripts/ffmpeg +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Sets up ffmpeg. - -# Stop on errors -set -e - -PACKAGES=( - ffmpeg -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 23f55eea13f..0cb49fde54e 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -6,7 +6,6 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" @@ -27,6 +26,10 @@ PACKAGES=( libudev-dev # homeassistant.components.homekit_controller libmpc-dev libmpfr-dev libgmp-dev + # homeassistant.components.ffmpeg + ffmpeg + # homeassistant.components.sensor.iperf3 + iperf3 ) # Required debian packages for building dependencies @@ -40,6 +43,10 @@ PACKAGES_DEV=( apt-get update apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} +# This is a list of scripts that install additional dependencies. If you only +# need to install a package from the official debian repository, just add it +# to the list above. Only create a script if you need compiling, manually +# downloading or a 3th party repository. if [ "$INSTALL_TELLSTICK" == "yes" ]; then virtualization/Docker/scripts/tellstick fi @@ -48,10 +55,6 @@ if [ "$INSTALL_OPENALPR" == "yes" ]; then virtualization/Docker/scripts/openalpr fi -if [ "$INSTALL_FFMPEG" == "yes" ]; then - virtualization/Docker/scripts/ffmpeg -fi - if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi