mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
commit
8ceb57752b
12
.coveragerc
12
.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
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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:
|
||||
|
@ -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
|
||||
|
@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_SSOCR no
|
||||
#ENV INSTALL_IPERF3 no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
|
@ -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
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()]
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
@ -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')]
|
@ -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."""
|
||||
|
102
homeassistant/components/binary_sensor/rainmachine.py
Normal file
102
homeassistant/components/binary_sensor/rainmachine.py
Normal file
@ -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']
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
101
homeassistant/components/climate/homematicip_cloud.py
Normal file
101
homeassistant/components/climate/homematicip_cloud.py
Normal file
@ -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)
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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?
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
103
homeassistant/components/cover/ryobi_gdo.py
Normal file
103
homeassistant/components/cover/ryobi_gdo.py
Normal file
@ -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()
|
@ -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"
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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'
|
||||
|
142
homeassistant/components/homekit/type_media_players.py
Normal file
142
homeassistant/components/homekit/type_media_players.py
Normal file
@ -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
|
@ -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
|
||||
|
@ -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)):
|
||||
|
@ -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()
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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'
|
||||
|
153
homeassistant/components/hydrawise.py
Normal file
153
homeassistant/components/hydrawise.py
Normal file
@ -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: {}<br />'
|
||||
'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'),
|
||||
}
|
@ -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):
|
||||
|
158
homeassistant/components/light/lw12wifi.py
Normal file
158
homeassistant/components/light/lw12wifi.py
Normal file
@ -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
|
@ -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
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
||||
|
92
homeassistant/components/lock/xiaomi_aqara.py
Normal file
92
homeassistant/components/lock/xiaomi_aqara.py
Normal file
@ -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
|
@ -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)
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
61
homeassistant/components/notify/flock.py
Normal file
61
homeassistant/components/notify/flock.py
Normal file
@ -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)
|
@ -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)
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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}<br />'
|
||||
'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)
|
226
homeassistant/components/rainmachine/__init__.py
Normal file
226
homeassistant/components/rainmachine/__init__.py
Normal file
@ -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}<br />'
|
||||
'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
|
32
homeassistant/components/rainmachine/services.yaml
Normal file
32
homeassistant/components/rainmachine/services.yaml
Normal file
@ -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
|
@ -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__)
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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'])
|
||||
|
@ -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':
|
||||
|
@ -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'
|
||||
|
72
homeassistant/components/sensor/hydrawise.py
Normal file
72
homeassistant/components/sensor/hydrawise.py
Normal file
@ -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')]
|
195
homeassistant/components/sensor/iperf3.py
Normal file
195
homeassistant/components/sensor/iperf3.py
Normal file
@ -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
|
@ -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)
|
||||
|
@ -33,6 +33,7 @@ DIGITS = {
|
||||
SENSOR_MODELS = [
|
||||
'Ubiquiti mFi-THS',
|
||||
'Ubiquiti mFi-CS',
|
||||
'Ubiquiti mFi-DS',
|
||||
'Outlet',
|
||||
'Input Analog',
|
||||
'Input Digital',
|
||||
|
@ -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()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user