Merge pull request #23864 from home-assistant/rc

0.93.0
This commit is contained in:
Paulus Schoutsen 2019-05-16 07:08:27 +02:00 committed by GitHub
commit 584bfbaa76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
565 changed files with 23840 additions and 14595 deletions

View File

@ -90,7 +90,7 @@ jobs:
name: run static check
command: |
. venv/bin/activate
flake8
flake8 homeassistant tests script
- run:
name: run static type check

View File

@ -13,3 +13,4 @@ coverage:
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
comment:
require_changes: yes
branches: master

View File

@ -22,6 +22,7 @@ omit =
homeassistant/components/alarmdotcom/alarm_control_panel.py
homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/tts.py
homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/*
homeassistant/components/amcrest/*
homeassistant/components/ampio/*
@ -52,6 +53,7 @@ omit =
homeassistant/components/bbox/sensor.py
homeassistant/components/bh1750/sensor.py
homeassistant/components/bitcoin/sensor.py
homeassistant/components/bizkaibus/sensor.py
homeassistant/components/blink/*
homeassistant/components/blinksticklight/light.py
homeassistant/components/blinkt/light.py
@ -173,6 +175,7 @@ omit =
homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py
homeassistant/components/essent/sensor.py
homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/*
homeassistant/components/everlights/light.py
@ -275,9 +278,11 @@ omit =
homeassistant/components/imap_email_content/sensor.py
homeassistant/components/influxdb/sensor.py
homeassistant/components/insteon/*
homeassistant/components/incomfort/*
homeassistant/components/ios/*
homeassistant/components/iota/*
homeassistant/components/iperf3/*
homeassistant/components/iqvia/*
homeassistant/components/irish_rail_transport/sensor.py
homeassistant/components/iss/binary_sensor.py
homeassistant/components/isy994/*
@ -344,6 +349,7 @@ omit =
homeassistant/components/message_bird/notify.py
homeassistant/components/met/weather.py
homeassistant/components/meteo_france/*
homeassistant/components/meteoalarm/*
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py
@ -418,6 +424,7 @@ omit =
homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py
homeassistant/components/opple/light.py
homeassistant/components/orangepi_gpio/*
homeassistant/components/orvibo/switch.py
homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py
@ -440,7 +447,6 @@ omit =
homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/*
homeassistant/components/pollen/sensor.py
homeassistant/components/postnl/sensor.py
homeassistant/components/prezzibenzina/sensor.py
homeassistant/components/proliphix/climate.py
@ -449,6 +455,7 @@ omit =
homeassistant/components/proxy/camera.py
homeassistant/components/ps4/__init__.py
homeassistant/components/ps4/media_player.py
homeassistant/components/ptvsd/*
homeassistant/components/pulseaudio_loopback/switch.py
homeassistant/components/pushbullet/notify.py
homeassistant/components/pushbullet/sensor.py
@ -534,9 +541,7 @@ omit =
homeassistant/components/smappee/*
homeassistant/components/smtp/notify.py
homeassistant/components/snapcast/media_player.py
homeassistant/components/snmp/device_tracker.py
homeassistant/components/snmp/sensor.py
homeassistant/components/snmp/switch.py
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py
@ -561,6 +566,7 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthru/sensor.py
homeassistant/components/synology/camera.py

View File

@ -7,7 +7,7 @@
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
**Pull request with documentation for [home-assistant.io](https://github.com/home-assistant/home-assistant.io) (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
## Example entry for `configuration.yaml` (if applicable):
```yaml
@ -18,21 +18,18 @@
- [ ] The code change is tested and works locally.
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR.
- [ ] I have followed the [development checklist][dev-checklist]
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If the code communicates with devices, web services, or third-party tools:
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]).
- [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
- [ ] New files were added to `.coveragerc`.
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`.
- [ ] Untested files have been added to `.coveragerc`.
If the code does not interact with devices:
- [ ] Tests have been added to verify that the new code works.
[ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23
[manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html

14
.github/main.workflow vendored
View File

@ -1,14 +0,0 @@
workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" {
on = "issues"
resolves = "codeowners-mention"
}
workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" {
on = "pull_request"
resolves = "codeowners-mention"
}
action "codeowners-mention" {
uses = "home-assistant/codeowners-mention@master"
secrets = ["GITHUB_TOKEN"]
}

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
config/*
config2/*
tests/testing_config/deps
tests/testing_config/home-assistant.log
@ -84,7 +85,7 @@ Scripts/
# vimmy stuff
*.swp
*.swo
tags
ctags.tmp
# vagrant stuff

33
.travis.yml Normal file
View File

@ -0,0 +1,33 @@
sudo: false
dist: xenial
addons:
apt:
sources:
- sourceline: "ppa:jonathonf/ffmpeg-4"
packages:
- libudev-dev
- libavformat-dev
- libavcodec-dev
- libavdevice-dev
- libavutil-dev
- libswscale-dev
- libswresample-dev
- libavfilter-dev
matrix:
fast_finish: true
include:
- python: "3.5.3"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=pylint
- python: "3.5.3"
env: TOXENV=typing
- python: "3.5.3"
env: TOXENV=py35
- python: "3.7"
env: TOXENV=py37
cache: pip
install: pip install -U tox
language: python
script: travis_wait 40 tox --develop

View File

@ -21,6 +21,7 @@ homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480
homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya
homeassistant/components/api/* @home-assistant/core
homeassistant/components/arduino/* @fabaff
@ -32,6 +33,7 @@ homeassistant/components/automation/* @home-assistant/core
homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610
homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blink/* @fronzbot
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
homeassistant/components/braviatv/* @robbiet480
@ -66,10 +68,13 @@ homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/emby/* @mezz64
homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
homeassistant/components/ephember/* @ttroy50
homeassistant/components/epsonworkforce/* @ThaStealth
homeassistant/components/eq3btsmart/* @rytilahti
homeassistant/components/esphome/* @OttoWinter
homeassistant/components/essent/* @TheLastProject
homeassistant/components/evohome/* @zxdavb
homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes
homeassistant/components/fitbit/* @robbiet480
@ -80,6 +85,7 @@ homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/frontend/* @home-assistant/core
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff
homeassistant/components/gntp/* @robbiet480
@ -99,6 +105,7 @@ homeassistant/components/history_graph/* @andrey-git
homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @cdce8p
homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/html5/* @robbiet480
homeassistant/components/http/* @home-assistant/core
@ -106,6 +113,7 @@ homeassistant/components/huawei_lte/* @scop
homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob
homeassistant/components/ign_sismologia/* @exxamalte
homeassistant/components/incomfort/* @zxdavb
homeassistant/components/influxdb/* @fabaff
homeassistant/components/input_boolean/* @home-assistant/core
homeassistant/components/input_datetime/* @home-assistant/core
@ -115,6 +123,7 @@ homeassistant/components/input_text/* @home-assistant/core
homeassistant/components/integration/* @dgomes
homeassistant/components/ios/* @robbiet480
homeassistant/components/ipma/* @dgomes
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/knx/* @Julius2342
@ -137,6 +146,7 @@ homeassistant/components/matrix/* @tinloaf
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff
@ -150,24 +160,28 @@ homeassistant/components/nello/* @pschmitt
homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @awarecan
homeassistant/components/netdata/* @fabaff
homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nissan_leaf/* @filcole
homeassistant/components/nmbs/* @thibmaek
homeassistant/components/no_ip/* @fabaff
homeassistant/components/notify/* @flowolf
homeassistant/components/notify/* @home-assistant/core
homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nuki/* @pschmitt
homeassistant/components/ohmconnect/* @robbiet480
homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff
homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/core
homeassistant/components/panel_iframe/* @home-assistant/core
homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
homeassistant/components/pi_hole/* @fabaff
homeassistant/components/plant/* @ChristianKuehnel
homeassistant/components/point/* @fredrike
homeassistant/components/pollen/* @bachya
homeassistant/components/ps4/* @ktnrg45
homeassistant/components/ptvsd/* @swamp-ig
homeassistant/components/push/* @dgomes
homeassistant/components/pvoutput/* @fabaff
homeassistant/components/qnap/* @colinodell
@ -204,7 +218,9 @@ homeassistant/components/supla/* @mwegrzynek
homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
homeassistant/components/sytadin/* @gautric
@ -235,6 +251,7 @@ homeassistant/components/uptimerobot/* @ludeeus
homeassistant/components/utility_meter/* @dgomes
homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vizio/* @raman325
homeassistant/components/waqi/* @andrey-git
homeassistant/components/weather/* @fabaff
homeassistant/components/weblink/* @home-assistant/core
@ -245,7 +262,7 @@ homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @fattdev
homeassistant/components/xmpp/* @fabaff
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward

186
azure-pipelines.yml Normal file
View File

@ -0,0 +1,186 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
branches:
include:
- dev
tags:
include:
- '*'
variables:
- name: versionBuilder
value: '3.2'
- name: versionWheels
value: '0.3'
- group: docker
- group: wheels
- group: github
jobs:
- job: 'Wheels'
condition: eq(variables['Build.SourceBranchName'], 'dev')
timeoutInMinutes: 360
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 3
matrix:
amd64:
buildArch: 'amd64'
i386:
buildArch: 'i386'
armhf:
buildArch: 'armhf'
armv7:
buildArch: 'armv7'
aarch64:
buildArch: 'aarch64'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: 'Initial cross build'
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: 'Install ssh key'
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: 'Install wheels builder'
- script: |
cp requirements_all.txt requirements_hassio.txt
# Enable because we can build it
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
sed -i "s|# decora|decora|g" requirements_hassio.txt
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
# Disable because of error
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
displayName: 'Prepare requirements files for Hass.io'
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
--index https://wheels.hass.io \
--requirement requirements_hassio.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: 'Run wheels build'
- job: 'Release'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
timeoutInMinutes: 120
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 5
matrix:
amd64:
buildArch: 'amd64'
buildMachine: 'qemux86-64,intel-nuc'
i386:
buildArch: 'i386'
buildMachine: 'qemux86'
armhf:
buildArch: 'armhf'
buildMachine: 'qemuarm,raspberrypi'
armv7:
buildArch: 'armv7'
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
aarch64:
buildArch: 'aarch64'
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
steps:
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
displayName: 'Docker hub login'
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
displayName: 'Install Builder'
- script: |
set -e
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t generic --docker-hub homeassistant
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t machine --docker-hub homeassistant
displayName: 'Build Release'
- job: 'ReleasePublish'
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release'))
dependsOn:
- 'Release'
pool:
vmImage: 'ubuntu-16.04'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
git jq
git config --global user.name "Pascal Vizeli"
git config --global user.email "pvizeli@syshack.ch"
git config --global credential.helper store
echo "https://$(githubToken):x-oauth-basic@github.com > $HOME\.git-credentials
displayName: 'Install requirements'
- script: |
set -e
version="$(Build.SourceBranchName)"
git clone https://github.com/home-assistant/hassio-version
cd hassio-version
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
if [[ "$version" =~ b ]]; then
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
else
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
sed -i "s|$stable_version|$version|g" stable.json
fi
git commit -am "Bump Home Assistant $version"
git push

View File

@ -7,8 +7,9 @@ import platform
import subprocess
import sys
import threading
from typing import List, Dict, Any # noqa pylint: disable=unused-import
from typing import ( # noqa pylint: disable=unused-import
List, Dict, Any, TYPE_CHECKING
)
from homeassistant import monkey_patch
from homeassistant.const import (
@ -18,6 +19,9 @@ from homeassistant.const import (
RESTART_EXIT_CODE,
)
if TYPE_CHECKING:
from homeassistant import core
def set_loop() -> None:
"""Attempt to use uvloop."""
@ -86,10 +90,12 @@ def ensure_config_path(config_dir: str) -> None:
sys.exit(1)
def ensure_config_file(config_dir: str) -> str:
async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
-> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir)
config_path = await config_util.async_ensure_config_exists(
hass, config_dir)
if config_path is None:
print('Error getting configuration path')
@ -261,6 +267,7 @@ def cmdline() -> List[str]:
async def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int:
"""Set up HASS and run."""
# pylint: disable=redefined-outer-name
from homeassistant import bootstrap, core
hass = core.HomeAssistant()
@ -275,7 +282,7 @@ async def setup_and_run_hass(config_dir: str,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color)
else:
config_file = ensure_config_file(config_dir)
config_file = await ensure_config_file(hass, config_dir)
print('Config directory:', config_dir)
await bootstrap.async_from_config_file(
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
@ -390,7 +397,7 @@ def main() -> int:
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
return exit_code # type: ignore # mypy cannot yet infer it
return exit_code # type: ignore
if __name__ == "__main__":

View File

@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6']
REQUIREMENTS = ['pyotp==2.2.7']
CONF_MESSAGE = 'message'

View File

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1']
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)

View File

@ -11,6 +11,7 @@ from .models import PermissionLookup
from .types import PolicyType
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
from .merge import merge_policies # noqa
from .util import test_all
POLICY_SCHEMA = vol.Schema({
@ -29,6 +30,10 @@ class AbstractPermissions:
"""Return a function that can test entity access."""
raise NotImplementedError
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
raise NotImplementedError
def check_entity(self, entity_id: str, key: str) -> bool:
"""Check if we can access entity."""
entity_func = self._cached_entity_func
@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions):
self._policy = policy
self._perm_lookup = perm_lookup
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return test_all(self._policy.get(CAT_ENTITIES), key)
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES),
@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions):
# pylint: disable=no-self-use
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return True
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return lambda entity_id, key: True

View File

@ -3,6 +3,7 @@ from functools import wraps
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
from .const import SUBCAT_ALL
from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType
@ -96,3 +97,16 @@ def _gen_dict_test_func(
return schema.get(key)
return test_value
def test_all(policy: CategoryType, key: str) -> bool:
"""Test if a policy has an ALL access for a specific key."""
if not isinstance(policy, dict):
return bool(policy)
all_policy = policy.get(SUBCAT_ALL)
if not isinstance(all_policy, dict):
return bool(all_policy)
return all_policy.get(key, False)

View File

@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
# hass.data key for logging information.
DATA_LOGGING = 'logging'
DEBUGGER_INTEGRATIONS = {'ptvsd', }
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
STAGE_1_INTEGRATIONS = {
@ -306,6 +307,15 @@ async def _async_set_up_integrations(
"""Set up all the integrations."""
domains = _get_domains(hass, config)
# Start up debuggers. Start these first in case they want to wait.
debuggers = domains & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers)
await asyncio.gather(*[
async_setup_component(hass, domain, config)
for domain in debuggers])
domains -= DEBUGGER_INTEGRATIONS
# Resolve all dependencies of all components so we can find the logging
# and integrations that need faster initialization.
resolved_domains_task = asyncio.gather(*[
@ -339,7 +349,7 @@ async def _async_set_up_integrations(
stage_2_domains = domains - logging_domains - stage_1_domains
if logging_domains:
_LOGGER.debug("Setting up %s", logging_domains)
_LOGGER.info("Setting up %s", logging_domains)
await asyncio.gather(*[
async_setup_component(hass, domain, config)

View File

@ -31,9 +31,11 @@ CONF_ADS_TYPE = 'adstype'
CONF_ADS_VALUE = 'value'
CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_VAR_POSITION = 'adsvar_position'
STATE_KEY_STATE = 'state'
STATE_KEY_BRIGHTNESS = 'brightness'
STATE_KEY_POSITION = 'position'
DOMAIN = 'ads'

View File

@ -0,0 +1,165 @@
"""Support for ADS covers."""
import logging
import voluptuous as vol
from homeassistant.components.cover import (
PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
CoverDevice)
from homeassistant.const import (
CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
from . import CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, \
AdsEntity, STATE_KEY_STATE, STATE_KEY_POSITION
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS Cover'
CONF_ADS_VAR_SET_POS = 'adsvar_set_position'
CONF_ADS_VAR_OPEN = 'adsvar_open'
CONF_ADS_VAR_CLOSE = 'adsvar_close'
CONF_ADS_VAR_STOP = 'adsvar_stop'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the cover platform for ADS."""
ads_hub = hass.data[DATA_ADS]
ads_var_is_closed = config.get(CONF_ADS_VAR)
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
name = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS)
add_entities([AdsCover(ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class)])
class AdsCover(AdsEntity, CoverDevice):
"""Representation of ADS cover."""
def __init__(self, ads_hub,
ads_var_is_closed, ads_var_position,
ads_var_pos_set, ads_var_open,
ads_var_close, ads_var_stop, name, device_class):
"""Initialize AdsCover entity."""
super().__init__(ads_hub, name, ads_var_is_closed)
if self._ads_var is None:
if ads_var_position is not None:
self._unique_id = ads_var_position
elif ads_var_pos_set is not None:
self._unique_id = ads_var_pos_set
elif ads_var_open is not None:
self._unique_id = ads_var_open
self._state_dict[STATE_KEY_POSITION] = None
self._ads_var_position = ads_var_position
self._ads_var_pos_set = ads_var_pos_set
self._ads_var_open = ads_var_open
self._ads_var_close = ads_var_close
self._ads_var_stop = ads_var_stop
self._device_class = device_class
async def async_added_to_hass(self):
"""Register device notification."""
if self._ads_var is not None:
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
if self._ads_var_position is not None:
await self.async_initialize_device(self._ads_var_position,
self._ads_hub.PLCTYPE_BYTE,
STATE_KEY_POSITION)
@property
def device_class(self):
"""Return the class of this cover."""
return self._device_class
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._ads_var is not None:
return self._state_dict[STATE_KEY_STATE]
if self._ads_var_position is not None:
return self._state_dict[STATE_KEY_POSITION] == 0
return None
@property
def current_cover_position(self):
"""Return current position of cover."""
return self._state_dict[STATE_KEY_POSITION]
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
if self._ads_var_stop is not None:
supported_features |= SUPPORT_STOP
if self._ads_var_pos_set is not None:
supported_features |= SUPPORT_SET_POSITION
return supported_features
def stop_cover(self, **kwargs):
"""Fire the stop action."""
if self._ads_var_stop:
self._ads_hub.write_by_name(self._ads_var_stop, True,
self._ads_hub.PLCTYPE_BOOL)
def set_cover_position(self, **kwargs):
"""Set cover position."""
position = kwargs[ATTR_POSITION]
if self._ads_var_pos_set is not None:
self._ads_hub.write_by_name(self._ads_var_pos_set, position,
self._ads_hub.PLCTYPE_BYTE)
def open_cover(self, **kwargs):
"""Move the cover up."""
if self._ads_var_open is not None:
self._ads_hub.write_by_name(self._ads_var_open, True,
self._ads_hub.PLCTYPE_BOOL)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=100)
def close_cover(self, **kwargs):
"""Move the cover down."""
if self._ads_var_close is not None:
self._ads_hub.write_by_name(self._ads_var_close, True,
self._ads_hub.PLCTYPE_BOOL)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=0)
@property
def available(self):
"""Return False if state has not been updated yet."""
if self._ads_var is not None or self._ads_var_position is not None:
return self._state_dict[STATE_KEY_STATE] is not None or \
self._state_dict[STATE_KEY_POSITION] is not None
return True

View File

@ -449,9 +449,9 @@ class _AlexaPowerController(_AlexaInterface):
if name != 'powerState':
raise _UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return 'ON'
if self.entity.state == STATE_OFF:
return 'OFF'
return 'ON'
class _AlexaLockController(_AlexaInterface):

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.",
"already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.",
"no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Autenticaci\u00f3 exitosa amb Ambi Climate."
},
"error": {
"follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia",
"no_token": "No autenticat amb Ambi Climate"
},
"step": {
"auth": {
"description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i <b>Permet</b> l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem <b>Envia</b> (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
"title": "Autenticaci\u00f3 amb Ambi Climate"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"error": {
"follow_link": "N\u00e1sledujte odkaz a prove\u010fte ov\u011b\u0159en\u00ed p\u0159ed stisknut\u00edm tla\u010d\u00edtka Odeslat.",
"no_token": "Nen\u00ed ov\u011b\u0159en s Ambiclimate"
},
"step": {
"auth": {
"description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a <b> Povolit </b> p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte <b> Odeslat </b> n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )",
"title": "Ov\u011b\u0159it Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.",
"already_setup": "Das Ambiclimate Konto ist konfiguriert.",
"no_config": "Ambiclimate muss konfiguriert sein, bevor die Authentifizierund durchgef\u00fchrt werden kann. [Bitte lies die Anleitung] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
},
"error": {
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",
"no_token": "Nicht authentifiziert mit Ambiclimate"
},
"step": {
"auth": {
"description": "Bitte folge diesem [link] ({authorization_url}) und <b> Erlaube </b> Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke <b> Senden </b> darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)",
"title": "Ambiclimate authentifizieren"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Unknown error generating an access token.",
"already_setup": "The Ambiclimate account is configured.",
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Successfully authenticated with Ambiclimate"
},
"error": {
"follow_link": "Please follow the link and authenticate before pressing Submit",
"no_token": "Not authenticated with Ambiclimate"
},
"step": {
"auth": {
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})",
"title": "Authenticate Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
},
"create_entry": {
"default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
"follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
"no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
},
"step": {
"auth": {
"description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 <b>\ud5c8\uc6a9</b> \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 <b>Submit</b> \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
"title": "Ambi Climate \uc778\uc99d"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.",
"already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.",
"no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)."
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert."
},
"error": {
"follow_link": "Follegt w.e.g. dem Link an authentifiz\u00e9iert de Kont ier dir op ofsch\u00e9cken dr\u00e9ckt.",
"no_token": "Net mat Ambiclimate authentifiz\u00e9iert"
},
"step": {
"auth": {
"description": "Follegt d\u00ebsem [Link]({authorization_url}) an <b>erlaabtt</b> den Acc\u00e8s zu \u00e4rem Ambiclimate Kont , a kommt dann zer\u00e9ck heihin an dr\u00e9ck op <b>ofsch\u00e9cken</b> hei \u00ebnnen.\n(Stellt s\u00e9cher dass den Type vun Callback {cb_url} ass.)",
"title": "Ambiclimate authentifiz\u00e9ieren"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
"already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
"no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
"follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
"no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430."
},
"step": {
"auth": {
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 <b>\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435</b> \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 <b>\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c</b>. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
"title": "Ambi Climate"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002",
"already_setup": "Ambiclimate \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002"
},
"error": {
"follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002",
"no_token": "Ambiclimate \u672a\u6388\u6b0a"
},
"step": {
"auth": {
"description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078<b>\u5141\u8a31</b>\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684<b>\u50b3\u9001</b>\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09",
"title": "\u8a8d\u8b49 Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,44 @@
"""Support for Ambiclimate devices."""
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from . import config_flow
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
})
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up Ambiclimate components."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
config_flow.register_flow_implementation(
hass, conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET])
return True
async def async_setup_entry(hass, entry):
"""Set up Ambiclimate from a config entry."""
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'climate'))
return True

View File

@ -0,0 +1,230 @@
"""Support for Ambiclimate ac."""
import asyncio
import logging
import ambiclimate
import voluptuous as vol
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_ON_OFF, STATE_HEAT)
from homeassistant.const import ATTR_NAME
from homeassistant.const import (ATTR_TEMPERATURE,
STATE_OFF, TEMP_CELSIUS)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_ON_OFF)
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
SET_COMFORT_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
})
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ambicliamte device."""
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Ambicliamte device from config entry."""
config = entry.data
websession = async_get_clientsession(hass)
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
token_info = await store.async_load()
oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
config['callback_url'],
websession)
try:
_token_info = await oauth.refresh_access_token(token_info)
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to refresh access token")
return
if _token_info:
await store.async_save(token_info)
token_info = _token_info
data_connection = ambiclimate.AmbiclimateConnection(oauth,
token_info=token_info,
websession=websession)
if not await data_connection.find_devices():
_LOGGER.error("No devices found")
return
tasks = []
for heater in data_connection.get_devices():
tasks.append(heater.update_device_info())
await asyncio.wait(tasks)
devs = []
for heater in data_connection.get_devices():
devs.append(AmbiclimateEntity(heater, store))
async_add_entities(devs, True)
async def send_comfort_feedback(service):
"""Send comfort feedback."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_comfort_feedback(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_FEEDBACK,
send_comfort_feedback,
schema=SEND_COMFORT_FEEDBACK_SCHEMA)
async def set_comfort_mode(service):
"""Set comfort mode."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_comfort_mode()
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_MODE,
set_comfort_mode,
schema=SET_COMFORT_MODE_SCHEMA)
async def set_temperature_mode(service):
"""Set temperature mode."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_temperature_mode(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_TEMPERATURE_MODE,
set_temperature_mode,
schema=SET_TEMPERATURE_MODE_SCHEMA)
class AmbiclimateEntity(ClimateDevice):
"""Representation of a Ambiclimate Thermostat device."""
def __init__(self, heater, store):
"""Initialize the thermostat."""
self._heater = heater
self._store = store
self._data = {}
@property
def unique_id(self):
"""Return a unique ID."""
return self._heater.device_id
@property
def name(self):
"""Return the name of the entity."""
return self._heater.name
@property
def device_info(self):
"""Return the device info."""
return {
'identifiers': {
(DOMAIN, self.unique_id)
},
'name': self.name,
'manufacturer': 'Ambiclimate',
}
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def target_temperature(self):
"""Return the target temperature."""
return self._data.get('target_temperature')
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_temperature(self):
"""Return the current temperature."""
return self._data.get('temperature')
@property
def current_humidity(self):
"""Return the current humidity."""
return self._data.get('humidity')
@property
def is_on(self):
"""Return true if heater is on."""
return self._data.get('power', '').lower() == 'on'
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._heater.get_min_temp()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._heater.get_max_temp()
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def current_operation(self):
"""Return current operation."""
return STATE_HEAT if self.is_on else STATE_OFF
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._heater.set_target_temperature(temperature)
async def async_turn_on(self):
"""Turn device on."""
await self._heater.turn_on()
async def async_turn_off(self):
"""Turn device off."""
await self._heater.turn_off()
async def async_update(self):
"""Retrieve latest state."""
try:
token_info = await self._heater.control.refresh_access_token()
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to refresh access token")
return
if token_info:
await self._store.async_save(token_info)
self._data = await self._heater.update_device()

View File

@ -0,0 +1,153 @@
"""Config flow for Ambiclimate."""
import logging
import ambiclimate
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
_LOGGER = logging.getLogger(__name__)
@callback
def register_flow_implementation(hass, client_id, client_secret):
"""Register a ambiclimate implementation.
client_id: Client id.
client_secret: Client secret.
"""
hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {})
hass.data[DATA_AMBICLIMATE_IMPL] = {
CONF_CLIENT_ID: client_id,
CONF_CLIENT_SECRET: client_secret,
}
@config_entries.HANDLERS.register('ambiclimate')
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize flow."""
self._registered_view = False
self._oauth = None
async def async_step_user(self, user_input=None):
"""Handle external yaml configuration."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
if not config:
_LOGGER.debug("No config")
return self.async_abort(reason='no_config')
return await self.async_step_auth()
async def async_step_auth(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
errors = {}
if user_input is not None:
errors['base'] = 'follow_link'
if not self._registered_view:
self._generate_view()
return self.async_show_form(
step_id='auth',
description_placeholders={'authorization_url':
await self._get_authorize_url(),
'cb_url': self._cb_url()},
errors=errors,
)
async def async_step_code(self, code=None):
"""Received code for authentication."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
token_info = await self._get_token_info(code)
if token_info is None:
return self.async_abort(reason='access_token')
config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
config['callback_url'] = self._cb_url()
return self.async_create_entry(
title="Ambiclimate",
data=config,
)
async def _get_token_info(self, code):
oauth = self._generate_oauth()
try:
token_info = await oauth.get_access_token(code)
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to get access token", exc_info=True)
return None
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
await store.async_save(token_info)
return token_info
def _generate_view(self):
self.hass.http.register_view(AmbiclimateAuthCallbackView())
self._registered_view = True
def _generate_oauth(self):
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
clientsession = async_get_clientsession(self.hass)
callback_url = self._cb_url()
oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
callback_url,
clientsession)
return oauth
def _cb_url(self):
return '{}{}'.format(self.hass.config.api.base_url,
AUTH_CALLBACK_PATH)
async def _get_authorize_url(self):
oauth = self._generate_oauth()
return oauth.get_authorize_url()
class AmbiclimateAuthCallbackView(HomeAssistantView):
"""Ambiclimate Authorization Callback View."""
requires_auth = False
url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
async def get(self, request):
"""Receive authorization token."""
code = request.query.get('code')
if code is None:
return "No code"
hass = request.app['hass']
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': 'code'},
data=code,
))
return "OK!"

View File

@ -0,0 +1,14 @@
"""Constants used by the Ambiclimate component."""
ATTR_VALUE = 'value'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
DOMAIN = 'ambiclimate'
SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
SERVICE_COMFORT_MODE = 'set_comfort_mode'
SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
STORAGE_KEY = 'ambiclimate_auth'
STORAGE_VERSION = 1
AUTH_CALLBACK_NAME = 'api:ambiclimate'
AUTH_CALLBACK_PATH = '/api/ambiclimate'

View File

@ -0,0 +1,12 @@
{
"domain": "ambiclimate",
"name": "Ambiclimate",
"documentation": "https://www.home-assistant.io/components/ambiclimate",
"requirements": [
"ambiclimate==0.1.1"
],
"dependencies": [],
"codeowners": [
"@danielhiversen"
]
}

View File

@ -0,0 +1,36 @@
# Describes the format for available services for ambiclimate
set_comfort_mode:
description: >
Enable comfort mode on your AC
fields:
Name:
description: >
String with device name.
example: Bedroom
send_comfort_feedback:
description: >
Send feedback for comfort mode
fields:
Name:
description: >
String with device name.
example: Bedroom
Value:
description: >
Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
example: bit_warm
set_temperature_mode:
description: >
Enable temperature mode on your AC
fields:
Name:
description: >
String with device name.
example: Bedroom
Value:
description: >
Target value in celsius
example: 22

View File

@ -0,0 +1,23 @@
{
"config": {
"title": "Ambiclimate",
"step": {
"auth": {
"title": "Authenticate Ambiclimate",
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})"
}
},
"create_entry": {
"default": "Successfully authenticated with Ambiclimate"
},
"error": {
"no_token": "Not authenticated with Ambiclimate",
"follow_link": "Please follow the link and authenticate before pressing Submit"
},
"abort": {
"already_setup": "The Ambiclimate account is configured.",
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
"access_token": "Unknown error generating an access token."
}
}
}

View File

@ -5,16 +5,30 @@ from datetime import timedelta
import aiohttp
import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.camera import DOMAIN as CAMERA
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
HTTP_BASIC_AUTHENTICATION)
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
from .binary_sensor import BINARY_SENSORS
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .const import DOMAIN, DATA_AMCREST
from .helpers import service_signal
from .sensor import SENSOR_MOTION_DETECTOR, SENSORS
from .switch import SWITCHES
_LOGGER = logging.getLogger(__name__)
CONF_AUTHENTICATION = 'authentication'
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'snapshot'
DEFAULT_ARGUMENTS = '-pred 1'
TIMEOUT = 10
DATA_AMCREST = 'amcrest'
DOMAIN = 'amcrest'
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
@ -43,48 +52,35 @@ AUTHENTICATION_LIST = {
'basic': 'basic'
}
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
BINARY_SENSORS = {
'motion_detected': 'Motion Detected'
}
# Sensor types are defined like: Name, units, icon
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSORS = {
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
def _deprecated_sensors(value):
if SENSOR_MOTION_DETECTOR in value:
def _deprecated_sensor_values(sensors):
if SENSOR_MOTION_DETECTOR in sensors:
_LOGGER.warning(
'sensors option %s is deprecated. '
'Please remove from your configuration and '
'use binary_sensors option motion_detected instead.',
SENSOR_MOTION_DETECTOR)
return value
"The 'sensors' option value '%s' is deprecated, "
"please remove it from your configuration and use "
"the 'binary_sensors' option with value 'motion_detected' "
"instead.", SENSOR_MOTION_DETECTOR)
return sensors
def _has_unique_names(value):
names = [camera[CONF_NAME] for camera in value]
def _deprecated_switches(config):
if CONF_SWITCHES in config:
_LOGGER.warning(
"The 'switches' option (with value %s) is deprecated, "
"please remove it from your configuration and use "
"camera services and attributes instead.",
config[CONF_SWITCHES])
return config
def _has_unique_names(devices):
names = [device[CONF_NAME] for device in devices]
vol.Schema(vol.Unique())(names)
return value
return devices
AMCREST_SCHEMA = vol.Schema({
AMCREST_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
@ -94,7 +90,7 @@ AMCREST_SCHEMA = vol.Schema({
vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
cv.string,
@ -103,10 +99,13 @@ AMCREST_SCHEMA = vol.Schema({
vol.Optional(CONF_BINARY_SENSORS):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
vol.All(cv.ensure_list, [vol.In(SENSORS)],
_deprecated_sensor_values),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})
}),
_deprecated_switches
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
@ -117,21 +116,22 @@ def setup(hass, config):
"""Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera, AmcrestError
hass.data.setdefault(DATA_AMCREST, {})
amcrest_cams = config[DOMAIN]
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
devices = config[DOMAIN]
for device in amcrest_cams:
for device in devices:
name = device[CONF_NAME]
username = device[CONF_USERNAME]
password = device[CONF_PASSWORD]
try:
camera = AmcrestCamera(device[CONF_HOST],
api = AmcrestCamera(device[CONF_HOST],
device[CONF_PORT],
username,
password).camera
# pylint: disable=pointless-statement
camera.current_time
# Test camera communications.
api.current_time
except AmcrestError as ex:
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
@ -148,7 +148,7 @@ def setup(hass, config):
binary_sensors = device.get(CONF_BINARY_SENSORS)
sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
stream_source = device[CONF_STREAM_SOURCE]
# currently aiohttp only works with basic authentication
# only valid for mjpeg streaming
@ -157,47 +157,97 @@ def setup(hass, config):
else:
authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source,
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
api, authentication, ffmpeg_arguments, stream_source,
resolution)
discovery.load_platform(
hass, 'camera', DOMAIN, {
hass, CAMERA, DOMAIN, {
CONF_NAME: name,
}, config)
if binary_sensors:
discovery.load_platform(
hass, 'binary_sensor', DOMAIN, {
hass, BINARY_SENSOR, DOMAIN, {
CONF_NAME: name,
CONF_BINARY_SENSORS: binary_sensors
}, config)
if sensors:
discovery.load_platform(
hass, 'sensor', DOMAIN, {
hass, SENSOR, DOMAIN, {
CONF_NAME: name,
CONF_SENSORS: sensors,
}, config)
if switches:
discovery.load_platform(
hass, 'switch', DOMAIN, {
hass, SWITCH, DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
return len(hass.data[DATA_AMCREST]) >= 1
if not hass.data[DATA_AMCREST]['devices']:
return False
def have_permission(user, entity_id):
return not user or user.permissions.check_entity(
entity_id, POLICY_CONTROL)
async def async_extract_from_service(call):
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id for entity_id in hass.data[DATA_AMCREST]['cameras']
if have_permission(user, entity_id)
]
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST]['cameras']:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context,
entity_id=entity_id,
permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call):
args = []
for arg in CAMERA_SERVICES[call.service][2]:
args.append(call.data[arg])
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(
hass,
service_signal(call.service, entity_id),
*args
)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(
DOMAIN, service, async_service_handler, params[0])
return True
class AmcrestDevice:
"""Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments,
def __init__(self, api, authentication, ffmpeg_arguments,
stream_source, resolution):
"""Initialize the entity."""
self.device = camera
self.name = name
self.api = api
self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source

View File

@ -5,38 +5,39 @@ import logging
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASS_MOTION)
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
from . import DATA_AMCREST, BINARY_SENSORS
from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
BINARY_SENSORS = {
'motion_detected': 'Motion Detected'
}
async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up a binary sensor for an Amcrest IP Camera."""
if discovery_info is None:
return
device_name = discovery_info[CONF_NAME]
binary_sensors = discovery_info[CONF_BINARY_SENSORS]
amcrest = hass.data[DATA_AMCREST][device_name]
amcrest_binary_sensors = []
for sensor_type in binary_sensors:
amcrest_binary_sensors.append(
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
async_add_devices(amcrest_binary_sensors, True)
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST]['devices'][name]
async_add_entities(
[AmcrestBinarySensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
True)
class AmcrestBinarySensor(BinarySensorDevice):
"""Binary sensor for Amcrest camera."""
def __init__(self, name, camera, sensor_type):
def __init__(self, name, device, sensor_type):
"""Initialize entity."""
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
self._camera = camera
self._api = device.api
self._sensor_type = sensor_type
self._state = None
@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
try:
self._state = self._camera.is_motion_detected
self._state = self._api.is_motion_detected
except AmcrestError as error:
_LOGGER.error(
'Could not update %s binary sensor due to error: %s',

View File

@ -2,18 +2,72 @@
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.camera import (
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM)
Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import CONF_NAME
from homeassistant.const import (
CONF_NAME, STATE_ON, STATE_OFF)
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
async_get_clientsession)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT
from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST
from .helpers import service_signal
_LOGGER = logging.getLogger(__name__)
STREAM_SOURCE_LIST = [
'snapshot',
'mjpeg',
'rtsp',
]
_SRV_EN_REC = 'enable_recording'
_SRV_DS_REC = 'disable_recording'
_SRV_EN_AUD = 'enable_audio'
_SRV_DS_AUD = 'disable_audio'
_SRV_EN_MOT_REC = 'enable_motion_recording'
_SRV_DS_MOT_REC = 'disable_motion_recording'
_SRV_GOTO = 'goto_preset'
_SRV_CBW = 'set_color_bw'
_SRV_TOUR_ON = 'start_tour'
_SRV_TOUR_OFF = 'stop_tour'
_ATTR_PRESET = 'preset'
_ATTR_COLOR_BW = 'color_bw'
_CBW_COLOR = 'color'
_CBW_AUTO = 'auto'
_CBW_BW = 'bw'
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
})
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
})
CAMERA_SERVICES = {
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
_SRV_EN_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
_SRV_DS_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
}
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities,
if discovery_info is None:
return
device_name = discovery_info[CONF_NAME]
amcrest = hass.data[DATA_AMCREST][device_name]
async_add_entities([AmcrestCam(hass, amcrest)], True)
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST]['devices'][name]
async_add_entities([
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera."""
def __init__(self, hass, amcrest):
def __init__(self, name, device, ffmpeg):
"""Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__()
self._name = amcrest.name
self._camera = amcrest.device
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
self._stream_source = amcrest.stream_source
self._resolution = amcrest.resolution
self._token = self._auth = amcrest.authentication
super().__init__()
self._name = name
self._api = device.api
self._ffmpeg = ffmpeg
self._ffmpeg_arguments = device.ffmpeg_arguments
self._stream_source = device.stream_source
self._resolution = device.resolution
self._token = self._auth = device.authentication
self._is_recording = False
self._motion_detection_enabled = None
self._model = None
self._audio_enabled = None
self._motion_recording_enabled = None
self._color_bw = None
self._snapshot_lock = asyncio.Lock()
self._unsub_dispatcher = []
async def async_camera_image(self):
"""Return a still image response from the camera."""
@ -56,7 +115,7 @@ class AmcrestCam(Camera):
try:
# Send the request to snap a picture and return raw jpg data
response = await self.hass.async_add_executor_job(
self._camera.snapshot, self._resolution)
self._api.snapshot, self._resolution)
return response.data
except AmcrestError as error:
_LOGGER.error(
@ -67,15 +126,16 @@ class AmcrestCam(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""
# The snapshot implementation is handled by the parent class
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
if self._stream_source == 'snapshot':
return await super().handle_async_mjpeg_stream(request)
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
if self._stream_source == 'mjpeg':
# stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
streaming_url, auth=self._token,
timeout=CAMERA_WEB_SESSION_TIMEOUT)
return await async_aiohttp_proxy_web(
self.hass, request, stream_coro)
@ -83,7 +143,7 @@ class AmcrestCam(Camera):
# streaming via ffmpeg
from haffmpeg.camera import CameraMjpeg
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
streaming_url = self._api.rtsp_url(typeno=self._resolution)
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments)
@ -103,6 +163,19 @@ class AmcrestCam(Camera):
"""Return the name of this camera."""
return self._name
@property
def device_state_attributes(self):
"""Return the Amcrest-specific camera state attributes."""
attr = {}
if self._audio_enabled is not None:
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
if self._motion_recording_enabled is not None:
attr['motion_recording'] = _BOOL_TO_STATE.get(
self._motion_recording_enabled)
if self._color_bw is not None:
attr[_ATTR_COLOR_BW] = self._color_bw
return attr
@property
def supported_features(self):
"""Return supported features."""
@ -120,6 +193,11 @@ class AmcrestCam(Camera):
"""Return the camera brand."""
return 'Amcrest'
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return self._motion_detection_enabled
@property
def model(self):
"""Return the camera model."""
@ -128,7 +206,7 @@ class AmcrestCam(Camera):
@property
def stream_source(self):
"""Return the source of the stream."""
return self._camera.rtsp_url(typeno=self._resolution)
return self._api.rtsp_url(typeno=self._resolution)
@property
def is_on(self):
@ -137,6 +215,21 @@ class AmcrestCam(Camera):
# Other Entity method overrides
async def async_added_to_hass(self):
"""Subscribe to signals and add camera to list."""
for service, params in CAMERA_SERVICES.items():
self._unsub_dispatcher.append(async_dispatcher_connect(
self.hass,
service_signal(service, self.entity_id),
getattr(self, params[1])))
self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id)
async def async_will_remove_from_hass(self):
"""Remove camera from list and disconnect from signals."""
self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id)
for unsub_dispatcher in self._unsub_dispatcher:
unsub_dispatcher()
def update(self):
"""Update entity status."""
from amcrest import AmcrestError
@ -144,15 +237,21 @@ class AmcrestCam(Camera):
_LOGGER.debug('Pulling data from %s camera', self.name)
if self._model is None:
try:
self._model = self._camera.device_type.split('=')[-1].strip()
self._model = self._api.device_type.split('=')[-1].strip()
except AmcrestError as error:
_LOGGER.error(
'Could not get %s camera model due to error: %s',
self.name, error)
self._model = ''
try:
self.is_streaming = self._camera.video_enabled
self._is_recording = self._camera.record_mode == 'Manual'
self.is_streaming = self._api.video_enabled
self._is_recording = self._api.record_mode == 'Manual'
self._motion_detection_enabled = (
self._api.is_motion_detector_on())
self._audio_enabled = self._api.audio_enabled
self._motion_recording_enabled = (
self._api.is_record_on_motion_detection())
self._color_bw = _CBW[self._api.day_night_color]
except AmcrestError as error:
_LOGGER.error(
'Could not get %s camera attributes due to error: %s',
@ -168,14 +267,71 @@ class AmcrestCam(Camera):
"""Turn on camera."""
self._enable_video_stream(True)
# Utility methods
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
self._enable_motion_detection(True)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
self._enable_motion_detection(False)
# Additional Amcrest Camera service methods
async def async_enable_recording(self):
"""Call the job and enable recording."""
await self.hass.async_add_executor_job(self._enable_recording, True)
async def async_disable_recording(self):
"""Call the job and disable recording."""
await self.hass.async_add_executor_job(self._enable_recording, False)
async def async_enable_audio(self):
"""Call the job and enable audio."""
await self.hass.async_add_executor_job(self._enable_audio, True)
async def async_disable_audio(self):
"""Call the job and disable audio."""
await self.hass.async_add_executor_job(self._enable_audio, False)
async def async_enable_motion_recording(self):
"""Call the job and enable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
True)
async def async_disable_motion_recording(self):
"""Call the job and disable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
False)
async def async_goto_preset(self, preset):
"""Call the job and move camera to preset position."""
await self.hass.async_add_executor_job(self._goto_preset, preset)
async def async_set_color_bw(self, color_bw):
"""Call the job and set camera color mode."""
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
async def async_start_tour(self):
"""Call the job and start camera tour."""
await self.hass.async_add_executor_job(self._start_tour, True)
async def async_stop_tour(self):
"""Call the job and stop camera tour."""
await self.hass.async_add_executor_job(self._start_tour, False)
# Methods to send commands to Amcrest camera and handle errors
def _enable_video_stream(self, enable):
"""Enable or disable camera video stream."""
from amcrest import AmcrestError
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# recording on if video stream is being turned off.
if self.is_recording and not enable:
self._enable_recording(False)
try:
self._camera.video_enabled = enable
self._api.video_enabled = enable
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera video stream due to error: %s',
@ -183,3 +339,103 @@ class AmcrestCam(Camera):
else:
self.is_streaming = enable
self.schedule_update_ha_state()
def _enable_recording(self, enable):
"""Turn recording on or off."""
from amcrest import AmcrestError
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# video stream off if recording is being turned on.
if not self.is_streaming and enable:
self._enable_video_stream(True)
rec_mode = {'Automatic': 0, 'Manual': 1}
try:
self._api.record_mode = rec_mode[
'Manual' if enable else 'Automatic']
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera recording due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._is_recording = enable
self.schedule_update_ha_state()
def _enable_motion_detection(self, enable):
"""Enable or disable motion detection."""
from amcrest import AmcrestError
try:
self._api.motion_detection = str(enable).lower()
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera motion detection due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._motion_detection_enabled = enable
self.schedule_update_ha_state()
def _enable_audio(self, enable):
"""Enable or disable audio stream."""
from amcrest import AmcrestError
try:
self._api.audio_enabled = enable
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera audio stream due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._audio_enabled = enable
self.schedule_update_ha_state()
def _enable_motion_recording(self, enable):
"""Enable or disable motion recording."""
from amcrest import AmcrestError
try:
self._api.motion_recording = str(enable).lower()
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera motion recording due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._motion_recording_enabled = enable
self.schedule_update_ha_state()
def _goto_preset(self, preset):
"""Move camera position and zoom to preset."""
from amcrest import AmcrestError
try:
self._api.go_to_preset(
action='start', preset_point_number=preset)
except AmcrestError as error:
_LOGGER.error(
'Could not move %s camera to preset %i due to error: %s',
self.name, preset, error)
def _set_color_bw(self, cbw):
"""Set camera color mode."""
from amcrest import AmcrestError
try:
self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error:
_LOGGER.error(
'Could not set %s camera color mode to %s due to error: %s',
self.name, cbw, error)
else:
self._color_bw = cbw
self.schedule_update_ha_state()
def _start_tour(self, start):
"""Start camera tour."""
from amcrest import AmcrestError
try:
self._api.tour(start=start)
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera tour due to error: %s',
'start' if start else 'stop', self.name, error)

View File

@ -0,0 +1,7 @@
"""Constants for amcrest component."""
DOMAIN = 'amcrest'
DATA_AMCREST = DOMAIN
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
CAMERA_WEB_SESSION_TIMEOUT = 10
SENSOR_SCAN_INTERVAL_SECS = 10

View File

@ -0,0 +1,10 @@
"""Helpers for amcrest component."""
from .const import DOMAIN
def service_signal(service, entity_id=None):
"""Encode service and entity_id into signal."""
signal = '{}_{}'.format(DOMAIN, service)
if entity_id:
signal += '_{}'.format(entity_id.replace('.', '_'))
return signal

View File

@ -3,7 +3,7 @@
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/components/amcrest",
"requirements": [
"amcrest==1.3.0"
"amcrest==1.4.0"
],
"dependencies": [
"ffmpeg"

View File

@ -5,11 +5,19 @@ import logging
from homeassistant.const import CONF_NAME, CONF_SENSORS
from homeassistant.helpers.entity import Entity
from . import DATA_AMCREST, SENSORS
from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
# Sensor types are defined like: Name, units, icon
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSORS = {
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
async def async_setup_platform(
@ -18,30 +26,26 @@ async def async_setup_platform(
if discovery_info is None:
return
device_name = discovery_info[CONF_NAME]
sensors = discovery_info[CONF_SENSORS]
amcrest = hass.data[DATA_AMCREST][device_name]
amcrest_sensors = []
for sensor_type in sensors:
amcrest_sensors.append(
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
async_add_entities(amcrest_sensors, True)
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST]['devices'][name]
async_add_entities(
[AmcrestSensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_SENSORS]],
True)
class AmcrestSensor(Entity):
"""A sensor implementation for Amcrest IP camera."""
def __init__(self, name, camera, sensor_type):
def __init__(self, name, device, sensor_type):
"""Initialize a sensor for Amcrest camera."""
self._attrs = {}
self._camera = camera
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
self._api = device.api
self._sensor_type = sensor_type
self._name = '{0}_{1}'.format(
name, SENSORS.get(self._sensor_type)[0])
self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
self._state = None
self._attrs = {}
self._unit_of_measurement = SENSORS[sensor_type][1]
self._icon = SENSORS[sensor_type][2]
@property
def name(self):
@ -66,22 +70,30 @@ class AmcrestSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
return SENSORS.get(self._sensor_type)[1]
return self._unit_of_measurement
def update(self):
"""Get the latest data and updates the state."""
_LOGGER.debug("Pulling data from %s sensor.", self._name)
if self._sensor_type == 'motion_detector':
self._state = self._camera.is_motion_detected
self._attrs['Record Mode'] = self._camera.record_mode
self._state = self._api.is_motion_detected
self._attrs['Record Mode'] = self._api.record_mode
elif self._sensor_type == 'ptz_preset':
self._state = self._camera.ptz_presets_count
self._state = self._api.ptz_presets_count
elif self._sensor_type == 'sdcard':
sd_used = self._camera.storage_used
sd_total = self._camera.storage_total
self._attrs['Total'] = '{0} {1}'.format(*sd_total)
self._attrs['Used'] = '{0} {1}'.format(*sd_used)
self._state = self._camera.storage_used_percent
storage = self._api.storage_all
try:
self._attrs['Total'] = '{:.2f} {}'.format(*storage['total'])
except ValueError:
self._attrs['Total'] = '{} {}'.format(*storage['total'])
try:
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
except ValueError:
self._attrs['Used'] = '{} {}'.format(*storage['used'])
try:
self._state = '{:.2f}'.format(storage['used_percent'])
except ValueError:
self._state = storage['used_percent']

View File

@ -0,0 +1,75 @@
enable_recording:
description: Enable continuous recording to camera storage.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_recording:
description: Disable continuous recording to camera storage.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
enable_audio:
description: Enable audio stream.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_audio:
description: Disable audio stream.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
enable_motion_recording:
description: Enable recording a clip to camera storage when motion is detected.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_motion_recording:
description: Disable recording a clip to camera storage when motion is detected.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
goto_preset:
description: Move camera to PTZ preset.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
preset:
description: Preset number, starting from 1.
example: 1
set_color_bw:
description: Set camera color mode.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
color_bw:
description: Color mode, one of 'auto', 'color' or 'bw'.
example: auto
start_tour:
description: Start camera's PTZ tour function.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
stop_tour:
description: Stop camera's PTZ tour function.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'

View File

@ -1,13 +1,19 @@
"""Support for toggling Amcrest IP camera settings."""
import logging
from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON
from homeassistant.const import CONF_NAME, CONF_SWITCHES
from homeassistant.helpers.entity import ToggleEntity
from . import DATA_AMCREST, SWITCHES
from .const import DATA_AMCREST
_LOGGER = logging.getLogger(__name__)
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
@ -16,67 +22,58 @@ async def async_setup_platform(
return
name = discovery_info[CONF_NAME]
switches = discovery_info[CONF_SWITCHES]
camera = hass.data[DATA_AMCREST][name].device
all_switches = []
for setting in switches:
all_switches.append(AmcrestSwitch(setting, camera, name))
async_add_entities(all_switches, True)
device = hass.data[DATA_AMCREST]['devices'][name]
async_add_entities(
[AmcrestSwitch(name, device, setting)
for setting in discovery_info[CONF_SWITCHES]],
True)
class AmcrestSwitch(ToggleEntity):
"""Representation of an Amcrest IP camera switch."""
def __init__(self, setting, camera, name):
def __init__(self, name, device, setting):
"""Initialize the Amcrest switch."""
self._name = '{} {}'.format(name, SWITCHES[setting][0])
self._api = device.api
self._setting = setting
self._camera = camera
self._name = '{} {}'.format(SWITCHES[setting][0], name)
self._state = False
self._icon = SWITCHES[setting][1]
self._state = None
@property
def name(self):
"""Return the name of the switch if any."""
return self._name
@property
def state(self):
"""Return the state of the switch."""
return self._state
@property
def is_on(self):
"""Return true if switch is on."""
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
"""Turn setting on."""
if self._setting == 'motion_detection':
self._camera.motion_detection = 'true'
self._api.motion_detection = 'true'
elif self._setting == 'motion_recording':
self._camera.motion_recording = 'true'
self._api.motion_recording = 'true'
def turn_off(self, **kwargs):
"""Turn setting off."""
if self._setting == 'motion_detection':
self._camera.motion_detection = 'false'
self._api.motion_detection = 'false'
elif self._setting == 'motion_recording':
self._camera.motion_recording = 'false'
self._api.motion_recording = 'false'
def update(self):
"""Update setting state."""
_LOGGER.debug("Polling state for setting: %s ", self._name)
if self._setting == 'motion_detection':
detection = self._camera.is_motion_detector_on()
detection = self._api.is_motion_detector_on()
elif self._setting == 'motion_recording':
detection = self._camera.is_record_on_motion_detection()
detection = self._api.is_record_on_motion_detection()
self._state = STATE_ON if detection else STATE_OFF
self._state = detection
@property
def icon(self):

View File

@ -21,11 +21,11 @@
},
"totp": {
"error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, por favor aseg\u00farate de que el reloj de tu Home Assistant es correcto."
"invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto."
},
"step": {
"init": {
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.",
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.",
"title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP"
}
},

View File

@ -97,8 +97,7 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request):
"""Get available auth providers."""
hass = request.app['hass']
if not hass.components.onboarding.async_is_onboarded():
if not hass.components.onboarding.async_is_user_onboarded():
return self.json_message(
message='Onboarding not finished',
status_code=400,

View File

@ -31,6 +31,6 @@ async def async_trigger(hass, config, action, automation_info):
'from_state': from_s,
'to_state': to_s,
},
}, context=to_s.context))
}, context=(to_s.context if to_s else None)))
return async_track_template(hass, value_template, template_listener)

View File

@ -1,7 +1,7 @@
"""Constants for the Axis component."""
import logging
LOGGER = logging.getLogger('homeassistant.components.axis')
LOGGER = logging.getLogger(__package__)
DOMAIN = 'axis'

View File

@ -0,0 +1 @@
"""The Bizkaibus bus tracker component."""

View File

@ -0,0 +1,8 @@
{
"domain": "bizkaibus",
"name": "Bizkaibus",
"documentation": "https://www.home-assistant.io/components/bizkaibus",
"dependencies": [],
"codeowners": ["@UgaitzEtxebarria"],
"requirements": ["bizkaibus==0.1.1"]
}

View File

@ -0,0 +1,88 @@
"""Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service."""
import logging
import voluptuous as vol
from bizkaibus.bizkaibus import BizkaibusData
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_DUE_IN = 'Due in'
CONF_STOP_ID = 'stopid'
CONF_ROUTE = 'route'
DEFAULT_NAME = 'Next bus'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STOP_ID): cv.string,
vol.Required(CONF_ROUTE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Bizkaibus public transport sensor."""
name = config.get(CONF_NAME)
stop = config[CONF_STOP_ID]
route = config[CONF_ROUTE]
data = Bizkaibus(stop, route)
add_entities([BizkaibusSensor(data, stop, route, name)], True)
class BizkaibusSensor(Entity):
"""The class for handling the data."""
def __init__(self, data, stop, route, name):
"""Initialize the sensor."""
self.data = data
self.stop = stop
self.route = route
self._name = name
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of the sensor."""
return 'minutes'
def update(self):
"""Get the latest data from the webservice."""
self.data.update()
try:
self._state = self.data.info[0][ATTR_DUE_IN]
except TypeError:
pass
class Bizkaibus:
"""The class for handling the data retrieval."""
def __init__(self, stop, route):
"""Initialize the data object."""
self.stop = stop
self.route = route
self.info = None
def update(self):
"""Retrieve the information from API."""
bridge = BizkaibusData(self.stop, self.route)
bridge.getNextBus()
self.info = bridge.info

View File

@ -3,7 +3,7 @@
"name": "Bluesound",
"documentation": "https://www.home-assistant.io/components/bluesound",
"requirements": [
"xmltodict==0.11.0"
"xmltodict==0.12.0"
],
"dependencies": [],
"codeowners": []

View File

@ -3,7 +3,7 @@
"name": "Bom",
"documentation": "https://www.home-assistant.io/components/bom",
"requirements": [
"bomradarloop==0.1.2"
"bomradarloop==0.1.3"
],
"dependencies": [],
"codeowners": []

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components.calendar import (
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, dt
@ -36,7 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SEARCH): cv.string,
})
]))
])),
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean
})
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
@ -50,7 +51,8 @@ def setup_platform(hass, config, add_entities, disc_info=None):
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
client = caldav.DAVClient(url, None, username, password)
client = caldav.DAVClient(url, None, username, password,
ssl_verify_cert=config.get(CONF_VERIFY_SSL))
calendars = client.principal().calendars()

View File

@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_create_catching_coro
from . import DOMAIN as CAST_DOMAIN
@ -522,8 +523,8 @@ class CastDevice(MediaPlayerDevice):
if _is_matching_dynamic_group(self._cast_info, discover):
_LOGGER.debug("Discovered matching dynamic group: %s",
discover)
self.hass.async_create_task(
self.async_set_dynamic_group(discover))
self.hass.async_create_task(async_create_catching_coro(
self.async_set_dynamic_group(discover)))
return
if self._cast_info.uuid != discover.uuid:
@ -536,7 +537,8 @@ class CastDevice(MediaPlayerDevice):
self._cast_info.host, self._cast_info.port)
return
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
self.hass.async_create_task(self.async_set_cast_info(discover))
self.hass.async_create_task(async_create_catching_coro(
self.async_set_cast_info(discover)))
def async_cast_removed(discover: ChromecastInfo):
"""Handle removal of Chromecast."""
@ -546,13 +548,15 @@ class CastDevice(MediaPlayerDevice):
if (self._dynamic_group_cast_info is not None and
self._dynamic_group_cast_info.uuid == discover.uuid):
_LOGGER.debug("Removed matching dynamic group: %s", discover)
self.hass.async_create_task(self.async_del_dynamic_group())
self.hass.async_create_task(async_create_catching_coro(
self.async_del_dynamic_group()))
return
if self._cast_info.uuid != discover.uuid:
# Removed is not our device.
return
_LOGGER.debug("Removed chromecast with same UUID: %s", discover)
self.hass.async_create_task(self.async_del_cast_info(discover))
self.hass.async_create_task(async_create_catching_coro(
self.async_del_cast_info(discover)))
async def async_stop(event):
"""Disconnect socket on Home Assistant stop."""
@ -565,14 +569,15 @@ class CastDevice(MediaPlayerDevice):
self.hass, SIGNAL_CAST_REMOVED,
async_cast_removed)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
self.hass.async_create_task(self.async_set_cast_info(self._cast_info))
self.hass.async_create_task(async_create_catching_coro(
self.async_set_cast_info(self._cast_info)))
for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]:
if _is_matching_dynamic_group(self._cast_info, info):
_LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s",
self.entity_id, self._cast_info.friendly_name,
self._cast_info.host, self._cast_info.port, info)
self.hass.async_create_task(
self.async_set_dynamic_group(info))
self.hass.async_create_task(async_create_catching_coro(
self.async_set_dynamic_group(info)))
break
async def async_will_remove_from_hass(self) -> None:
@ -1046,6 +1051,11 @@ class CastDevice(MediaPlayerDevice):
return images[0].url if images and images[0].url else None
@property
def media_image_remotely_accessible(self) -> bool:
"""If the image url is remotely accessible."""
return True
@property
def media_title(self):
"""Title of current playing media."""

View File

@ -3,7 +3,9 @@ import logging
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME,\
CONF_MAXIMUM, CONF_MINIMUM
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
@ -12,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
ATTR_INITIAL = 'initial'
ATTR_STEP = 'step'
ATTR_MINIMUM = 'minimum'
ATTR_MAXIMUM = 'maximum'
CONF_INITIAL = 'initial'
CONF_RESTORE = 'restore'
@ -26,11 +30,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_DECREMENT = 'decrement'
SERVICE_INCREMENT = 'increment'
SERVICE_RESET = 'reset'
SERVICE_CONFIGURE = 'configure'
SERVICE_SCHEMA = vol.Schema({
SERVICE_SCHEMA_SIMPLE = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
})
SERVICE_SCHEMA_CONFIGURE = vol.Schema({
ATTR_ENTITY_ID: cv.comp_entity_ids,
vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_STEP): cv.positive_int,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.schema_with_slug_keys(
vol.Any({
@ -38,6 +50,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MAXIMUM, default=None):
vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_MINIMUM, default=None):
vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_RESTORE, default=True): cv.boolean,
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
}, None)
@ -60,21 +76,27 @@ async def async_setup(hass, config):
restore = cfg.get(CONF_RESTORE)
step = cfg.get(CONF_STEP)
icon = cfg.get(CONF_ICON)
minimum = cfg.get(CONF_MINIMUM)
maximum = cfg.get(CONF_MAXIMUM)
entities.append(Counter(object_id, name, initial, restore, step, icon))
entities.append(Counter(object_id, name, initial, minimum, maximum,
restore, step, icon))
if not entities:
return False
component.async_register_entity_service(
SERVICE_INCREMENT, SERVICE_SCHEMA,
SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE,
'async_increment')
component.async_register_entity_service(
SERVICE_DECREMENT, SERVICE_SCHEMA,
SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE,
'async_decrement')
component.async_register_entity_service(
SERVICE_RESET, SERVICE_SCHEMA,
SERVICE_RESET, SERVICE_SCHEMA_SIMPLE,
'async_reset')
component.async_register_entity_service(
SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE,
'async_configure')
await component.async_add_entities(entities)
return True
@ -83,13 +105,16 @@ async def async_setup(hass, config):
class Counter(RestoreEntity):
"""Representation of a counter."""
def __init__(self, object_id, name, initial, restore, step, icon):
def __init__(self, object_id, name, initial, minimum, maximum,
restore, step, icon):
"""Initialize a counter."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self._restore = restore
self._step = step
self._state = self._initial = initial
self._min = minimum
self._max = maximum
self._icon = icon
@property
@ -115,10 +140,24 @@ class Counter(RestoreEntity):
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ret = {
ATTR_INITIAL: self._initial,
ATTR_STEP: self._step,
}
if self._min is not None:
ret[CONF_MINIMUM] = self._min
if self._max is not None:
ret[CONF_MAXIMUM] = self._max
return ret
def compute_next_state(self, state):
"""Keep the state within the range of min/max values."""
if self._min is not None:
state = max(self._min, state)
if self._max is not None:
state = min(self._max, state)
return state
async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
@ -128,19 +167,31 @@ class Counter(RestoreEntity):
if self._restore:
state = await self.async_get_last_state()
if state is not None:
self._state = int(state.state)
self._state = self.compute_next_state(int(state.state))
async def async_decrement(self):
"""Decrement the counter."""
self._state -= self._step
self._state = self.compute_next_state(self._state - self._step)
await self.async_update_ha_state()
async def async_increment(self):
"""Increment a counter."""
self._state += self._step
self._state = self.compute_next_state(self._state + self._step)
await self.async_update_ha_state()
async def async_reset(self):
"""Reset a counter."""
self._state = self._initial
self._state = self.compute_next_state(self._initial)
await self.async_update_ha_state()
async def async_configure(self, **kwargs):
"""Change the counter's settings with a service."""
if CONF_MINIMUM in kwargs:
self._min = kwargs[CONF_MINIMUM]
if CONF_MAXIMUM in kwargs:
self._max = kwargs[CONF_MAXIMUM]
if CONF_STEP in kwargs:
self._step = kwargs[CONF_STEP]
self._state = self.compute_next_state(self._state)
await self.async_update_ha_state()

View File

@ -18,3 +18,18 @@ reset:
entity_id:
description: Entity id of the counter to reset.
example: 'counter.count0'
configure:
description: Change counter parameters
fields:
entity_id:
description: Entity id of the counter to change.
example: 'counter.count0'
minimum:
description: New minimum value for the counter or None to remove minimum
example: 0
maximum:
description: New maximum value for the counter or None to remove maximum
example: 100
step:
description: New value for step
example: 2

View File

@ -6,9 +6,10 @@ import voluptuous as vol
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE,
ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY,
STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE,
ATTR_AWAY_MODE, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE,
ATTR_OPERATION_MODE, ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY,
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS)
@ -44,6 +45,7 @@ DAIKIN_TO_HA_STATE = {
}
HA_ATTR_TO_DAIKIN = {
ATTR_AWAY_MODE: 'en_hol',
ATTR_OPERATION_MODE: 'mode',
ATTR_FAN_MODE: 'f_rate',
ATTR_SWING_MODE: 'f_dir',
@ -93,8 +95,9 @@ class DaikinClimate(ClimateDevice):
),
}
self._supported_features = SUPPORT_TARGET_TEMPERATURE \
self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
| SUPPORT_OPERATION_MODE
| SUPPORT_TARGET_TEMPERATURE)
if self._api.device.support_fan_mode:
self._supported_features |= SUPPORT_FAN_MODE
@ -266,3 +269,36 @@ class DaikinClimate(ClimateDevice):
def device_info(self):
"""Return a device description for device registry."""
return self._api.device_info
@property
def is_on(self):
"""Return true if on."""
return self._api.device.represent(
HA_ATTR_TO_DAIKIN[ATTR_OPERATION_MODE]
)[1] != HA_STATE_TO_DAIKIN[STATE_OFF]
async def async_turn_on(self):
"""Turn device on."""
await self._api.device.set({})
async def async_turn_off(self):
"""Turn device off."""
await self._api.device.set({
HA_ATTR_TO_DAIKIN[ATTR_OPERATION_MODE]:
HA_STATE_TO_DAIKIN[STATE_OFF]
})
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._api.device.represent(
HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]
)[1] != HA_STATE_TO_DAIKIN[STATE_OFF]
async def async_turn_away_mode_on(self):
"""Turn away mode on."""
await self._api.device.set({HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]: '1'})
async def async_turn_away_mode_off(self):
"""Turn away mode off."""
await self._api.device.set({HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]: '0'})

View File

@ -85,5 +85,9 @@ class DanfossAir:
= self._client.command(ReadCommand.boost)
self._data[ReadCommand.battery_percent] \
= self._client.command(ReadCommand.battery_percent)
self._data[ReadCommand.bypass] \
= self._client.command(ReadCommand.bypass)
self._data[ReadCommand.automatic_bypass] \
= self._client.command(ReadCommand.automatic_bypass)
_LOGGER.debug("Done fetching data from Danfoss Air CCM module")

View File

@ -3,7 +3,7 @@
"name": "Danfoss air",
"documentation": "https://www.home-assistant.io/components/danfoss_air",
"requirements": [
"pydanfossair==0.0.7"
"pydanfossair==0.1.0"
],
"dependencies": [],
"codeowners": []

View File

@ -19,6 +19,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ReadCommand.boost,
UpdateCommand.boost_activate,
UpdateCommand.boost_deactivate],
["Danfoss Air Bypass",
ReadCommand.bypass,
UpdateCommand.bypass_activate,
UpdateCommand.bypass_deactivate],
["Danfoss Air Automatic Bypass",
ReadCommand.automatic_bypass,
UpdateCommand.bypass_activate,
UpdateCommand.bypass_deactivate],
]
dev = []
@ -59,7 +67,7 @@ class DanfossAir(SwitchDevice):
def turn_off(self, **kwargs):
"""Turn the switch off."""
_LOGGER.debug("Turning of switch with command %s", self._off_command)
_LOGGER.debug("Turning off switch with command %s", self._off_command)
self._data.update_state(self._off_command, self._state_command)
def update(self):

View File

@ -26,7 +26,7 @@
"title": "Definici\u00f3 de la passarel\u00b7la deCONZ"
},
"link": {
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"",
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"",
"title": "Vincular amb deCONZ"
},
"options": {

View File

@ -12,7 +12,7 @@
"init": {
"data": {
"host": "Hostitel",
"port": "Port (v\u00fdchoz\u00ed hodnota: '80')"
"port": "Port"
},
"title": "Definujte br\u00e1nu deCONZ"
},

View File

@ -3,7 +3,7 @@
"abort": {
"already_configured": "El puente ya esta configurado",
"no_bridges": "No se han descubierto puentes deCONZ",
"one_instance_only": "El componente s\u00f3lo soporta una instancia deCONZ",
"one_instance_only": "El componente solo admite una instancia de deCONZ",
"updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host"
},
"error": {
@ -26,7 +26,7 @@
"title": "Definir pasarela deCONZ"
},
"link": {
"description": "Desbloquee su pasarela deCONZ para registrarse con Home Assistant. \n\n 1. Ir a la configuraci\u00f3n del sistema deCONZ \n 2. Presione el bot\u00f3n \"Desbloquear Gateway\"",
"description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"",
"title": "Enlazar con deCONZ"
},
"options": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_configured": "Broen er allerede konfigurert",
"no_bridges": "Ingen deCONZ broer oppdaget",
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst"
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst",
"updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse"
},
"error": {
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
@ -25,7 +26,7 @@
"title": "Definer deCONZ-gatewayen"
},
"link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
"title": "Koble til deCONZ"
},
"options": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_configured": "Mostek jest ju\u017c skonfigurowany",
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ"
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ",
"updated_instance": "Zaktualizowano instancj\u0119 deCONZ o nowy adres hosta"
},
"error": {
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"

View File

@ -4,7 +4,7 @@
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ",
"updated_instance": "deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d \u0441 \u043d\u043e\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0445\u043e\u0441\u0442\u0430"
"updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d"
},
"error": {
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"

View File

@ -3,7 +3,8 @@
"abort": {
"already_configured": "Most je \u017ee nastavljen",
"no_bridges": "Ni odkritih mostov deCONZ",
"one_instance_only": "Komponenta podpira le en primerek deCONZ"
"one_instance_only": "Komponenta podpira le en primerek deCONZ",
"updated_instance": "Posodobljen deCONZ z novim naslovom gostitelja"
},
"error": {
"no_key": "Klju\u010da API ni mogo\u010de dobiti"

View File

@ -9,6 +9,12 @@
"no_key": "Det gick inte att ta emot en API-nyckel"
},
"step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
},
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
},
"init": {
"data": {
"host": "V\u00e4rd",

View File

@ -88,9 +88,11 @@ class DeconzCover(DeconzDevice, CoverDevice):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
data = {'on': False}
if position > 0:
data['on'] = True
data['bri'] = int(position / 100 * 255)
await self._device.async_set_state(data)
async def async_open_cover(self, **kwargs):
@ -126,7 +128,9 @@ class DeconzCoverZigbeeSpec(DeconzCover):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
data = {'on': False}
if position < 100:
data['on'] = True
data['bri'] = 255 - int(position / 100 * 255)
await self._device.async_set_state(data)

View File

@ -61,8 +61,10 @@ class DeconzDevice(Entity):
if (self._device.uniqueid is None or
self._device.uniqueid.count(':') != 7):
return None
serial = self._device.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -165,6 +165,8 @@ class DeconzLight(DeconzDevice, Light):
"""Return the device state attributes."""
attributes = {}
attributes['is_deconz_group'] = self._device.type == 'LightGroup'
if self._device.type == 'LightGroup':
attributes['all_on'] = self._device.all_on
return attributes

View File

@ -3,7 +3,7 @@
"name": "Deconz",
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
"pydeconz==54"
"pydeconz==58"
],
"dependencies": [],
"codeowners": [

View File

@ -51,7 +51,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
# We only implement the methods that we support
def __init__(self, name):
def __init__(self, name, device_class=None):
"""Initialize the demo device."""
self._name = name
self._player_state = STATE_PLAYING
@ -60,6 +60,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
self._shuffle = False
self._sound_mode_list = SOUND_MODE_LIST
self._sound_mode = DEFAULT_SOUND_MODE
self._device_class = device_class
@property
def should_poll(self):
@ -101,6 +102,11 @@ class AbstractDemoPlayer(MediaPlayerDevice):
"""Return a list of available sound modes."""
return self._sound_mode_list
@property
def device_class(self):
"""Return the device class of the media player."""
return self._device_class
def turn_on(self):
"""Turn the media player on."""
self._player_state = STATE_PLAYING

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
"not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
"one_instance_allowed": "Solo una instancia es necesaria."
},
"create_entry": {
@ -9,7 +9,8 @@
},
"step": {
"user": {
"description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?"
"description": "\u00bfEst\u00e1s seguro de que quieres configurar Dialogflow?",
"title": "Configurar el Webhook de Dialogflow"
}
},
"title": "Dialogflow"

View File

@ -3,7 +3,7 @@
"name": "Discord",
"documentation": "https://www.home-assistant.io/components/discord",
"requirements": [
"discord.py==0.16.12"
"discord.py==1.0.1"
],
"dependencies": [],
"codeowners": []

View File

@ -1,5 +1,6 @@
"""Discord platform for notify component."""
import logging
import os.path
import voluptuous as vol
@ -33,36 +34,69 @@ class DiscordNotificationService(BaseNotificationService):
self.token = token
self.hass = hass
def file_exists(self, filename):
"""Check if a file exists on disk and is in authorized path."""
if not self.hass.config.is_allowed_path(filename):
return False
return os.path.isfile(filename)
async def async_send_message(self, message, **kwargs):
"""Login to Discord, send message to channel(s) and log out."""
import discord
discord.VoiceClient.warn_nacl = False
discord_bot = discord.Client(loop=self.hass.loop)
images = None
if ATTR_TARGET not in kwargs:
_LOGGER.error("No target specified")
return None
if ATTR_DATA in kwargs:
data = kwargs.get(ATTR_DATA)
if ATTR_IMAGES in data:
images = list()
for image in data.get(ATTR_IMAGES):
image_exists = await self.hass.async_add_executor_job(
self.file_exists,
image)
if image_exists:
images.append(image)
else:
_LOGGER.warning("Image not found: %s", image)
# pylint: disable=unused-variable
@discord_bot.event
async def on_ready():
"""Send the messages when the bot is ready."""
try:
data = kwargs.get(ATTR_DATA)
images = None
if data:
images = data.get(ATTR_IMAGES)
for channelid in kwargs[ATTR_TARGET]:
channel = discord.Object(id=channelid)
await discord_bot.send_message(channel, message)
channelid = int(channelid)
channel = discord_bot.get_channel(channelid)
if channel is None:
_LOGGER.warning(
"Channel not found for id: %s",
channelid)
continue
# Must create new instances of File for each channel.
files = None
if images:
for anum, f_name in enumerate(images):
await discord_bot.send_file(channel, f_name)
files = list()
for image in images:
files.append(discord.File(image))
await channel.send(message, files=files)
except (discord.errors.HTTPException,
discord.errors.NotFound) as error:
_LOGGER.warning("Communication error: %s", error)
await discord_bot.logout()
await discord_bot.close()
await discord_bot.start(self.token)
# Using reconnect=False prevents multiple ready events to be fired.
await discord_bot.start(self.token, reconnect=False)

View File

@ -105,16 +105,19 @@ OPTIONAL_SERVICE_HANDLERS = {
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
}
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
vol.All(cv.ensure_list, [vol.In(DEFAULT_ENABLED)]),
vol.Optional(CONF_ENABLE, default=[]):
vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)])
vol.All(cv.ensure_list, [
vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)]),
}),
}, extra=vol.ALLOW_EXTRA)
@ -140,6 +143,14 @@ async def async_setup(hass, config):
ignored_platforms = []
enabled_platforms = []
for platform in enabled_platforms:
if platform in DEFAULT_ENABLED:
logger.warning(
"Please remove %s from your discovery.enable configuration "
"as it is now enabled by default",
platform,
)
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in ignored_platforms:

View File

@ -3,7 +3,7 @@
"name": "Dnsip",
"documentation": "https://www.home-assistant.io/components/dnsip",
"requirements": [
"aiodns==1.1.1"
"aiodns==2.0.0"
],
"dependencies": [],
"codeowners": []

View File

@ -1,26 +1,26 @@
"""Get your own public IP address or that of any host."""
import logging
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
CONF_HOSTNAME = 'hostname'
CONF_IPV6 = 'ipv6'
CONF_RESOLVER = 'resolver'
CONF_RESOLVER_IPV6 = 'resolver_ipv6'
CONF_IPV6 = 'ipv6'
DEFAULT_NAME = 'myip'
DEFAULT_HOSTNAME = 'myip.opendns.com'
DEFAULT_IPV6 = False
DEFAULT_NAME = 'myip'
DEFAULT_RESOLVER = '208.67.222.222'
DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2'
DEFAULT_IPV6 = False
SCAN_INTERVAL = timedelta(seconds=120)
@ -33,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the DNS IP sensor."""
hostname = config.get(CONF_HOSTNAME)
name = config.get(CONF_NAME)
@ -57,8 +57,9 @@ class WanIpSensor(Entity):
"""Implementation of a DNS IP sensor."""
def __init__(self, hass, name, hostname, resolver, ipv6):
"""Initialize the sensor."""
"""Initialize the DNS IP sensor."""
import aiodns
self.hass = hass
self._name = name
self.hostname = hostname
@ -80,9 +81,10 @@ class WanIpSensor(Entity):
async def async_update(self):
"""Get the current DNS IP address for hostname."""
from aiodns.error import DNSError
try:
response = await self.resolver.query(self.hostname,
self.querytype)
response = await self.resolver.query(
self.hostname, self.querytype)
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
response = None

View File

@ -16,6 +16,7 @@ CONF_RETRY = 'retry'
DEFAULT_TIMEOUT = 5
DEFAULT_RETRY = 10
DYSON_DEVICES = 'dyson_devices'
DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality']
DOMAIN = 'dyson'
@ -91,9 +92,7 @@ def setup(hass, config):
# Start fan/sensors components
if hass.data[DYSON_DEVICES]:
_LOGGER.debug("Starting sensor/fan components")
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
for platform in DYSON_PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True

View File

@ -0,0 +1,126 @@
"""Support for Dyson Pure Cool Air Quality Sensors."""
import logging
from homeassistant.components.air_quality import AirQualityEntity, DOMAIN
from . import DYSON_DEVICES
ATTRIBUTION = 'Dyson purifier air quality sensor'
_LOGGER = logging.getLogger(__name__)
DYSON_AIQ_DEVICES = 'dyson_aiq_devices'
ATTR_VOC = 'volatile_organic_compounds'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dyson Sensors."""
from libpurecool.dyson_pure_cool import DysonPureCool
if discovery_info is None:
return
hass.data.setdefault(DYSON_AIQ_DEVICES, [])
# Get Dyson Devices from parent component
device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]]
for device in hass.data[DYSON_DEVICES]:
if isinstance(device, DysonPureCool) and \
device.serial not in device_ids:
hass.data[DYSON_AIQ_DEVICES].append(DysonAirSensor(device))
add_entities(hass.data[DYSON_AIQ_DEVICES])
class DysonAirSensor(AirQualityEntity):
"""Representation of a generic Dyson air quality sensor."""
def __init__(self, device):
"""Create a new generic air quality Dyson sensor."""
self._device = device
self._old_value = None
self._name = device.name
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_executor_job(
self._device.add_message_listener, self.on_message)
def on_message(self, message):
"""Handle new messages which are received from the fan."""
from libpurecool.dyson_pure_state_v2 import \
DysonEnvironmentalSensorV2State
_LOGGER.debug('%s: Message received for %s device: %s',
DOMAIN, self.name, message)
if (self._old_value is None or
self._old_value != self._device.environmental_state) and \
isinstance(message, DysonEnvironmentalSensorV2State):
self._old_value = self._device.environmental_state
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the Dyson sensor."""
return self._name
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
return max(self.particulate_matter_2_5,
self.particulate_matter_10,
self.nitrogen_dioxide,
self.volatile_organic_compounds)
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
if self._device.environmental_state:
return int(self._device.environmental_state.particulate_matter_25)
return None
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
if self._device.environmental_state:
return int(self._device.environmental_state.particulate_matter_10)
return None
@property
def nitrogen_dioxide(self):
"""Return the NO2 (nitrogen dioxide) level."""
if self._device.environmental_state:
return int(self._device.environmental_state.nitrogen_dioxide)
return None
@property
def volatile_organic_compounds(self):
"""Return the VOC (Volatile Organic Compounds) level."""
if self._device.environmental_state:
return int(self._device.
environmental_state.volatile_organic_compounds)
return None
@property
def unique_id(self):
"""Return the sensor's unique id."""
return self._device.serial
@property
def device_state_attributes(self):
"""Return the device state attributes."""
data = {}
voc = self.volatile_organic_compounds
if voc is not None:
data[ATTR_VOC] = voc
return data

View File

@ -474,7 +474,8 @@ class DysonPureCoolDevice(FanEntity):
FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM,
FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM,
FanSpeed.FAN_SPEED_8.value: SPEED_HIGH,
FanSpeed.FAN_SPEED_9.value: SPEED_HIGH}
FanSpeed.FAN_SPEED_9.value: SPEED_HIGH,
FanSpeed.FAN_SPEED_10.value: SPEED_HIGH}
return speed_map[self._device.state.speed]

View File

@ -3,7 +3,6 @@ import logging
from homeassistant.const import STATE_OFF, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
from . import DYSON_DEVICES
SENSOR_UNITS = {
@ -21,21 +20,33 @@ SENSOR_ICONS = {
'temperature': 'mdi:thermometer',
}
DYSON_SENSOR_DEVICES = 'dyson_sensor_devices'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dyson Sensors."""
_LOGGER.debug("Creating new Dyson fans")
devices = []
unit = hass.config.units.temperature_unit
# Get Dyson Devices from parent component
from libpurecool.dyson_pure_cool import DysonPureCool
from libpurecool.dyson_pure_cool_link import DysonPureCoolLink
from libpurecool.dyson_pure_cool import DysonPureCool
for device in [d for d in hass.data[DYSON_DEVICES]
if isinstance(d, DysonPureCoolLink) and
not isinstance(d, DysonPureCool)]:
if discovery_info is None:
return
hass.data.setdefault(DYSON_SENSOR_DEVICES, [])
unit = hass.config.units.temperature_unit
devices = hass.data[DYSON_SENSOR_DEVICES]
# Get Dyson Devices from parent component
device_ids = [device.unique_id for device in
hass.data[DYSON_SENSOR_DEVICES]]
for device in hass.data[DYSON_DEVICES]:
if isinstance(device, DysonPureCool):
if '{}-{}'.format(device.serial, 'temperature') not in device_ids:
devices.append(DysonTemperatureSensor(device, unit))
if '{}-{}'.format(device.serial, 'humidity') not in device_ids:
devices.append(DysonHumiditySensor(device))
elif isinstance(device, DysonPureCoolLink):
devices.append(DysonFilterLifeSensor(device))
devices.append(DysonDustSensor(device))
devices.append(DysonHumiditySensor(device))
@ -56,7 +67,7 @@ class DysonSensor(Entity):
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_job(
self.hass.async_add_executor_job(
self._device.add_message_listener, self.on_message)
def on_message(self, message):
@ -88,6 +99,11 @@ class DysonSensor(Entity):
"""Return the icon for this sensor."""
return SENSOR_ICONS[self._sensor_type]
@property
def unique_id(self):
"""Return the sensor's unique id."""
return '{}-{}'.format(self._device.serial, self._sensor_type)
class DysonFilterLifeSensor(DysonSensor):
"""Representation of Dyson Filter Life sensor (in hours)."""

View File

@ -0,0 +1,6 @@
{
"state": {
"day": "Den",
"night": "Noc"
}
}

View File

@ -3,7 +3,7 @@
"name": "Econet",
"documentation": "https://www.home-assistant.io/components/econet",
"requirements": [
"pyeconet==0.0.10"
"pyeconet==0.0.11"
],
"dependencies": [],
"codeowners": []

View File

@ -5,7 +5,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import CoreState, EventOrigin
LOGGER = logging.getLogger('.')
LOGGER = logging.getLogger(__package__)
EVENT_ROKU_COMMAND = 'roku_command'

View File

@ -4,13 +4,13 @@ import logging
import voluptuous as vol
from homeassistant.const import CONF_DEVICE
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'enocean'
ENOCEAN_DONGLE = None
DATA_ENOCEAN = 'enocean'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -18,14 +18,15 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message'
SIGNAL_SEND_MESSAGE = 'enocean.send_message'
def setup(hass, config):
"""Set up the EnOcean component."""
global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE)
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
dongle = EnOceanDongle(hass, serial_dev)
hass.data[DATA_ENOCEAN] = dongle
return True
@ -39,87 +40,53 @@ class EnOceanDongle:
self.__communicator = SerialCommunicator(
port=ser, callback=self.callback)
self.__communicator.start()
self.__devices = []
self.hass = hass
self.hass.helpers.dispatcher.dispatcher_connect(
SIGNAL_SEND_MESSAGE, self._send_message_callback)
def register_device(self, dev):
"""Register another device."""
self.__devices.append(dev)
def send_command(self, command):
"""Send a command from the EnOcean dongle."""
def _send_message_callback(self, command):
"""Send a command through the EnOcean dongle."""
self.__communicator.send(command)
# pylint: disable=no-self-use
def _combine_hex(self, data):
"""Combine list of integer values to one big integer."""
output = 0x00
for i, j in enumerate(reversed(data)):
output |= (j << i * 8)
return output
def callback(self, temp):
def callback(self, packet):
"""Handle EnOcean device's callback.
This is the callback function called by python-enocan whenever there
is an incoming packet.
"""
from enocean.protocol.packet import RadioPacket
if isinstance(temp, RadioPacket):
_LOGGER.debug("Received radio packet: %s", temp)
rxtype = None
value = None
channel = 0
if temp.data[6] == 0x30:
rxtype = "wallswitch"
value = 1
elif temp.data[6] == 0x20:
rxtype = "wallswitch"
value = 0
elif temp.data[4] == 0x0c:
rxtype = "power"
value = temp.data[3] + (temp.data[2] << 8)
elif temp.data[2] & 0x60 == 0x60:
rxtype = "switch_status"
channel = temp.data[2] & 0x1F
if temp.data[3] == 0xe4:
value = 1
elif temp.data[3] == 0x80:
value = 0
elif temp.data[0] == 0xa5 and temp.data[1] == 0x02:
rxtype = "dimmerstatus"
value = temp.data[2]
for device in self.__devices:
if rxtype == "wallswitch" and device.stype == "listener":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value, temp.data[1])
if rxtype == "power" and device.stype == "powersensor":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "power" and device.stype == "switch":
if temp.sender_int == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch" and \
channel == device.channel:
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if isinstance(packet, RadioPacket):
_LOGGER.debug("Received radio packet: %s", packet)
self.hass.helpers.dispatcher.dispatcher_send(
SIGNAL_RECEIVE_MESSAGE, packet)
class EnOceanDevice():
class EnOceanDevice(Entity):
"""Parent class for all devices associated with the EnOcean component."""
def __init__(self):
def __init__(self, dev_id, dev_name="EnOcean device"):
"""Initialize the device."""
ENOCEAN_DONGLE.register_device(self)
self.stype = ""
self.sensorid = [0x00, 0x00, 0x00, 0x00]
self.dev_id = dev_id
self.dev_name = dev_name
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RECEIVE_MESSAGE, self._message_received_callback)
def _message_received_callback(self, packet):
"""Handle incoming packets."""
from enocean.utils import combine_hex
if packet.sender_int == combine_hex(self.dev_id):
self.value_changed(packet)
def value_changed(self, packet):
"""Update the internal state of the device when a packet arrives."""
# pylint: disable=no-self-use
def send_command(self, data, optional, packet_type):
"""Send a command via the EnOcean dongle."""
from enocean.protocol.packet import Packet
packet = Packet(packet_type, data=data, optional=optional)
ENOCEAN_DONGLE.send_command(packet)
self.hass.helpers.dispatcher.dispatcher_send(
SIGNAL_SEND_MESSAGE, packet)

View File

@ -3,16 +3,17 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.components import enocean
from homeassistant.const import (
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean binary sensor'
DEPENDENCIES = ['enocean']
EVENT_BUTTON_PRESSED = 'button_pressed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -24,61 +25,80 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform for EnOcean."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanBinarySensor(dev_id, devname, device_class)])
add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches."""
"""Representation of EnOcean binary sensors such as wall switches.
def __init__(self, dev_id, devname, device_class):
Supported EEPs (EnOcean Equipment Profiles):
- F6-02-01 (Light and Blind Control - Application Style 2)
- F6-02-02 (Light and Blind Control - Application Style 1)
"""
def __init__(self, dev_id, dev_name, device_class):
"""Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self)
self.stype = 'listener'
self.dev_id = dev_id
super().__init__(dev_id, dev_name)
self._device_class = device_class
self.which = -1
self.onoff = -1
self.devname = devname
self._device_class = device_class
@property
def name(self):
"""Return the default name for the binary sensor."""
return self.devname
return self.dev_name
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
def value_changed(self, value, value2):
def value_changed(self, packet):
"""Fire an event with the data that have changed.
This method is called when there is an incoming packet associated
with this platform.
Example packet data:
- 2nd button pressed
['0xf6', '0x10', '0x00', '0x2d', '0xcf', '0x45', '0x30']
- button released
['0xf6', '0x00', '0x00', '0x2d', '0xcf', '0x45', '0x20']
"""
# Energy Bow
pushed = None
if packet.data[6] == 0x30:
pushed = 1
elif packet.data[6] == 0x20:
pushed = 0
self.schedule_update_ha_state()
if value2 == 0x70:
action = packet.data[1]
if action == 0x70:
self.which = 0
self.onoff = 0
elif value2 == 0x50:
elif action == 0x50:
self.which = 0
self.onoff = 1
elif value2 == 0x30:
elif action == 0x30:
self.which = 1
self.onoff = 0
elif value2 == 0x10:
elif action == 0x10:
self.which = 1
self.onoff = 1
elif value2 == 0x37:
elif action == 0x37:
self.which = 10
self.onoff = 0
elif value2 == 0x15:
elif action == 0x15:
self.which = 10
self.onoff = 1
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
'pushed': value,
self.hass.bus.fire(EVENT_BUTTON_PRESSED,
{'id': self.dev_id,
'pushed': pushed,
'which': self.which,
'onoff': self.onoff})

View File

@ -4,10 +4,10 @@ import math
import voluptuous as vol
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean
from homeassistant.components.light import (
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@ -28,29 +28,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean light platform."""
sender_id = config.get(CONF_SENDER_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
dev_id = config.get(CONF_ID)
add_entities([EnOceanLight(sender_id, devname, dev_id)])
add_entities([EnOceanLight(sender_id, dev_id, dev_name)])
class EnOceanLight(enocean.EnOceanDevice, Light):
"""Representation of an EnOcean light source."""
def __init__(self, sender_id, devname, dev_id):
def __init__(self, sender_id, dev_id, dev_name):
"""Initialize the EnOcean light source."""
enocean.EnOceanDevice.__init__(self)
super().__init__(dev_id, dev_name)
self._on_state = False
self._brightness = 50
self._sender_id = sender_id
self.dev_id = dev_id
self._devname = devname
self.stype = 'dimmer'
@property
def name(self):
"""Return the name of the device if any."""
return self._devname
return self.dev_name
@property
def brightness(self):
@ -94,8 +91,14 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
self.send_command(command, [], 0x01)
self._on_state = False
def value_changed(self, val):
"""Update the internal state of this device."""
def value_changed(self, packet):
"""Update the internal state of this device.
Dimmer devices like Eltako FUD61 send telegram in different RORGs.
We only care about the 4BS (0xA5).
"""
if packet.data[0] == 0xa5 and packet.data[1] == 0x02:
val = packet.data[2]
self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0)
self.schedule_update_ha_state()

View File

@ -3,8 +3,8 @@
"name": "Enocean",
"documentation": "https://www.home-assistant.io/components/enocean",
"requirements": [
"enocean==0.40"
"enocean==0.50"
],
"dependencies": [],
"codeowners": []
"codeowners": ["@bdurrer"]
}

View File

@ -3,58 +3,201 @@ import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID, POWER_WATT)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components import enocean
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, POWER_WATT)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_MAX_TEMP = 'max_temp'
CONF_MIN_TEMP = 'min_temp'
CONF_RANGE_FROM = 'range_from'
CONF_RANGE_TO = 'range_to'
DEFAULT_NAME = 'EnOcean sensor'
DEVICE_CLASS_POWER = 'powersensor'
SENSOR_TYPES = {
DEVICE_CLASS_HUMIDITY: {
'name': 'Humidity',
'unit': '%',
'icon': 'mdi:water-percent',
'class': DEVICE_CLASS_HUMIDITY,
},
DEVICE_CLASS_POWER: {
'name': 'Power',
'unit': POWER_WATT,
'icon': 'mdi:power-plug',
'class': DEVICE_CLASS_POWER,
},
DEVICE_CLASS_TEMPERATURE: {
'name': 'Temperature',
'unit': TEMP_CELSIUS,
'icon': 'mdi:thermometer',
'class': DEVICE_CLASS_TEMPERATURE,
},
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string,
vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int),
vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int,
vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an EnOcean sensor device."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
dev_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanSensor(dev_id, devname)])
if dev_class == DEVICE_CLASS_TEMPERATURE:
temp_min = config.get(CONF_MIN_TEMP)
temp_max = config.get(CONF_MAX_TEMP)
range_from = config.get(CONF_RANGE_FROM)
range_to = config.get(CONF_RANGE_TO)
add_entities([EnOceanTemperatureSensor(
dev_id, dev_name, temp_min, temp_max, range_from, range_to)])
elif dev_class == DEVICE_CLASS_HUMIDITY:
add_entities([EnOceanHumiditySensor(dev_id, dev_name)])
elif dev_class == DEVICE_CLASS_POWER:
add_entities([EnOceanPowerSensor(dev_id, dev_name)])
class EnOceanSensor(enocean.EnOceanDevice, Entity):
class EnOceanSensor(enocean.EnOceanDevice):
"""Representation of an EnOcean sensor device such as a power meter."""
def __init__(self, dev_id, devname):
def __init__(self, dev_id, dev_name, sensor_type):
"""Initialize the EnOcean sensor device."""
enocean.EnOceanDevice.__init__(self)
self.stype = "powersensor"
self.power = None
self.dev_id = dev_id
self.which = -1
self.onoff = -1
self.devname = devname
super().__init__(dev_id, dev_name)
self._sensor_type = sensor_type
self._device_class = SENSOR_TYPES[self._sensor_type]['class']
self._dev_name = '{} {}'.format(
SENSOR_TYPES[self._sensor_type]['name'], dev_name)
self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
self._icon = SENSOR_TYPES[self._sensor_type]['icon']
self._state = None
@property
def name(self):
"""Return the name of the device."""
return 'Power %s' % self.devname
return self._dev_name
def value_changed(self, value):
"""Update the internal state of the device."""
self.power = value
self.schedule_update_ha_state()
@property
def icon(self):
"""Icon to use in the frontend."""
return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def state(self):
"""Return the state of the device."""
return self.power
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return POWER_WATT
return self._unit_of_measurement
def value_changed(self, packet):
"""Update the internal state of the sensor."""
class EnOceanPowerSensor(EnOceanSensor):
"""Representation of an EnOcean power sensor.
EEPs (EnOcean Equipment Profiles):
- A5-12-01 (Automated Meter Reading, Electricity)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean power sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
# this packet reports the current value
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
self._state = raw_val / (10 ** divisor)
self.schedule_update_ha_state()
class EnOceanTemperatureSensor(EnOceanSensor):
"""Representation of an EnOcean temperature sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-02-01 to A5-02-1B All 8 Bit Temperature Sensors of A5-02
- A5-10-01 to A5-10-14 (Room Operating Panels)
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 (Temp. and Humidity Sensor and Set Point)
- A5-10-12 (Temp. and Humidity Sensor, Set Point and Occupancy Control)
- 10 Bit Temp. Sensors are not supported (A5-02-20, A5-02-30)
For the following EEPs the scales must be set to "0 to 250":
- A5-04-01
- A5-04-02
- A5-10-10 to A5-10-14
"""
def __init__(self, dev_id, dev_name, scale_min, scale_max,
range_from, range_to):
"""Initialize the EnOcean temperature sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE)
self._scale_min = scale_min
self._scale_max = scale_max
self.range_from = range_from
self.range_to = range_to
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.data[0] != 0xa5:
return
temp_scale = self._scale_max - self._scale_min
temp_range = self.range_to - self.range_from
raw_val = packet.data[3]
temperature = temp_scale / temp_range * (raw_val - self.range_from)
temperature += self._scale_min
self._state = round(temperature, 1)
self.schedule_update_ha_state()
class EnOceanHumiditySensor(EnOceanSensor):
"""Representation of an EnOcean humidity sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 to A5-10-14 (Room Operating Panels)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean humidity sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
humidity = packet.data[2] * 100 / 250
self._state = round(humidity, 1)
self.schedule_update_ha_state()

View File

@ -3,16 +3,16 @@ import logging
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean Switch'
CONF_CHANNEL = 'channel'
DEFAULT_NAME = 'EnOcean Switch'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -23,26 +23,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean switch platform."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
channel = config.get(CONF_CHANNEL)
dev_id = config.get(CONF_ID)
dev_name = config.get(CONF_NAME)
add_entities([EnOceanSwitch(dev_id, devname, channel)])
add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
"""Representation of an EnOcean switch device."""
def __init__(self, dev_id, devname, channel):
def __init__(self, dev_id, dev_name, channel):
"""Initialize the EnOcean switch device."""
enocean.EnOceanDevice.__init__(self)
self.dev_id = dev_id
self._devname = devname
super().__init__(dev_id, dev_name)
self._light = None
self._on_state = False
self._on_state2 = False
self.channel = channel
self.stype = "switch"
@property
def is_on(self):
@ -52,7 +49,7 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
@property
def name(self):
"""Return the device name."""
return self._devname
return self.dev_name
def turn_on(self, **kwargs):
"""Turn on the switch."""
@ -74,7 +71,24 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
packet_type=0x01)
self._on_state = False
def value_changed(self, val):
def value_changed(self, packet):
"""Update the internal state of the switch."""
self._on_state = val
if packet.data[0] == 0xa5:
# power meter telegram, turn on if > 10 watts
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
watts = raw_val / (10 ** divisor)
if watts > 1:
self._on_state = True
self.schedule_update_ha_state()
elif packet.data[0] == 0xd2:
# actuator status telegram
packet.parse_eep(0x01, 0x01)
if packet.parsed['CMD']['raw_value'] == 4:
channel = packet.parsed['IO']['raw_value']
output = packet.parsed['OV']['raw_value']
if channel == self.channel:
self._on_state = output > 0
self.schedule_update_ha_state()

View File

@ -4,6 +4,6 @@
"documentation": "https://www.home-assistant.io/components/epsonworkforce",
"dependencies": [],
"codeowners": ["@ThaStealth"],
"requirements": ["epsonprinter==0.0.8"]
"requirements": ["epsonprinter==0.0.9"]
}

View File

@ -10,15 +10,16 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['epsonprinter==0.0.8']
REQUIREMENTS = ['epsonprinter==0.0.9']
_LOGGER = logging.getLogger(__name__)
MONITORED_CONDITIONS = {
'black': ['Ink level Black', '%', 'mdi:water'],
'photoblack': ['Ink level Photoblack', '%', 'mdi:water'],
'magenta': ['Ink level Magenta', '%', 'mdi:water'],
'cyan': ['Ink level Cyan', '%', 'mdi:water'],
'yellow': ['Ink level Yellow', '%', 'mdi:water'],
'clean': ['Inklevel Cleaning', '%', 'mdi:water'],
'clean': ['Cleaning level', '%', 'mdi:water'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,

View File

@ -13,7 +13,7 @@
"data": {
"password": "Contrasenya"
},
"description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3.",
"description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3 com a {name}.",
"title": "Introdueix la contrasenya"
},
"discovery_confirm": {

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"authenticate": {
"description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} ."
},
"discovery_confirm": {
"description": "Chcete do domovsk\u00e9ho asistenta p\u0159idat uzel ESPHome `{name}`?",
"title": "Nalezen uzel ESPHome"
}
}
}
}

View File

@ -4,14 +4,16 @@
"already_configured": "ESP ya est\u00e1 configurado"
},
"error": {
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!"
"connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.",
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!",
"resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"step": {
"authenticate": {
"data": {
"password": "Contrase\u00f1a"
},
"description": "Escribe la contrase\u00f1a que hayas establecido en tu configuraci\u00f3n.",
"description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}.",
"title": "Escribe la contrase\u00f1a"
},
"discovery_confirm": {
@ -23,6 +25,7 @@
"host": "Host",
"port": "Puerto"
},
"description": "Introduce la configuraci\u00f3n de la conexi\u00f3n de tu nodo [ESPHome](https://esphomelib.com/).",
"title": "ESPHome"
}
},

View File

@ -0,0 +1 @@
"""The Essent component."""

View File

@ -0,0 +1,8 @@
{
"domain": "essent",
"name": "Essent",
"documentation": "https://www.home-assistant.io/components/essent",
"requirements": ["PyEssent==0.10"],
"dependencies": [],
"codeowners": ["@TheLastProject"]
}

View File

@ -0,0 +1,112 @@
"""Support for Essent API."""
from datetime import timedelta
from pyessent import PyEssent
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
SCAN_INTERVAL = timedelta(hours=1)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Essent platform."""
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
essent = EssentBase(username, password)
meters = []
for meter in essent.retrieve_meters():
data = essent.retrieve_meter_data(meter)
for tariff in data['values']['LVR'].keys():
meters.append(EssentMeter(
essent,
meter,
data['type'],
tariff,
data['values']['LVR'][tariff]['unit']))
add_devices(meters, True)
class EssentBase():
"""Essent Base."""
def __init__(self, username, password):
"""Initialize the Essent API."""
self._username = username
self._password = password
self._meters = []
self._meter_data = {}
self.update()
def retrieve_meters(self):
"""Retrieve the list of meters."""
return self._meters
def retrieve_meter_data(self, meter):
"""Retrieve the data for this meter."""
return self._meter_data[meter]
@Throttle(timedelta(minutes=30))
def update(self):
"""Retrieve the latest meter data from Essent."""
essent = PyEssent(self._username, self._password)
self._meters = essent.get_EANs()
for meter in self._meters:
self._meter_data[meter] = essent.read_meter(
meter, only_last_meter_reading=True)
class EssentMeter(Entity):
"""Representation of Essent measurements."""
def __init__(self, essent_base, meter, meter_type, tariff, unit):
"""Initialize the sensor."""
self._state = None
self._essent_base = essent_base
self._meter = meter
self._type = meter_type
self._tariff = tariff
self._unit = unit
@property
def name(self):
"""Return the name of the sensor."""
return "Essent {} ({})".format(self._type, self._tariff)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self._unit.lower() == 'kwh':
return ENERGY_KILO_WATT_HOUR
return self._unit
def update(self):
"""Fetch the energy usage."""
# Ensure our data isn't too old
self._essent_base.update()
# Retrieve our meter
data = self._essent_base.retrieve_meter_data(self._meter)
# Set our value
self._state = next(
iter(data['values']['LVR'][self._tariff]['records'].values()))

View File

@ -5,26 +5,31 @@
# 0-12 Heating zones (a.k.a. Zone), and
# 0-1 DHW controller, (a.k.a. Boiler)
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
from datetime import timedelta
from datetime import datetime, timedelta
import logging
import requests.exceptions
import voluptuous as vol
import evohomeclient2
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_START)
EVENT_HOMEASSISTANT_START,
HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS,
PRECISION_HALVES, TEMP_CELSIUS)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from .const import (
DOMAIN, DATA_EVOHOME, DISPATCHER_EVOHOME, GWS, TCS)
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'evohome'
DATA_EVOHOME = 'data_' + DOMAIN
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
CONF_LOCATION_IDX = 'location_idx'
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
@ -43,10 +48,6 @@ CONF_SECRETS = [
CONF_USERNAME, CONF_PASSWORD,
]
# These are used to help prevent E501 (line too long) violations.
GWS = 'gateways'
TCS = 'temperatureControlSystems'
# bit masks for dispatcher packets
EVO_PARENT = 0x01
EVO_CHILD = 0x02
@ -66,8 +67,6 @@ def setup(hass, hass_config):
scan_interval = timedelta(
minutes=(scan_interval.total_seconds() + 59) // 60)
import evohomeclient2
try:
client = evo_data['client'] = evohomeclient2.EvohomeClient(
evo_data['params'][CONF_USERNAME],
@ -145,3 +144,129 @@ def setup(hass, hass_config):
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
return True
class EvoDevice(Entity):
"""Base for any Honeywell evohome device.
Such devices include the Controller, (up to 12) Heating Zones and
(optionally) a DHW controller.
"""
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome entity."""
self._client = client
self._obj = obj_ref
self._name = None
self._icon = None
self._type = None
self._supported_features = None
self._operation_list = None
self._params = evo_data['params']
self._timers = evo_data['timers']
self._status = {}
self._available = False # should become True after first update()
@callback
def _connect(self, packet):
if packet['to'] & self._type and packet['signal'] == 'refresh':
self.async_schedule_update_ha_state(force_refresh=True)
def _handle_exception(self, err):
try:
raise err
except evohomeclient2.AuthenticationError:
_LOGGER.error(
"Failed to (re)authenticate with the vendor's server. "
"This may be a temporary error. Message is: %s",
err
)
except requests.exceptions.ConnectionError:
# this appears to be common with Honeywell's servers
_LOGGER.warning(
"Unable to connect with the vendor's server. "
"Check your network and the vendor's status page."
)
except requests.exceptions.HTTPError:
if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
_LOGGER.warning(
"Vendor says their server is currently unavailable. "
"This may be temporary; check the vendor's status page."
)
elif err.response.status_code == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(
"The vendor's API rate limit has been exceeded. "
"So will cease polling, and will resume after %s seconds.",
(self._params[CONF_SCAN_INTERVAL] * 3).total_seconds()
)
self._timers['statusUpdated'] = datetime.now() + \
self._params[CONF_SCAN_INTERVAL] * 3
else:
raise # we don't expect/handle any other HTTPErrors
# These properties, methods are from the Entity class
async def async_added_to_hass(self):
"""Run when entity about to be added."""
async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect)
@property
def should_poll(self) -> bool:
"""Most evohome devices push their state to HA.
Only the Controller should be polled.
"""
return False
@property
def name(self) -> str:
"""Return the name to use in the frontend UI."""
return self._name
@property
def device_state_attributes(self):
"""Return the device state attributes of the evohome device.
This is state data that is not available otherwise, due to the
restrictions placed upon ClimateDevice properties, etc. by HA.
"""
return {'status': self._status}
@property
def icon(self):
"""Return the icon to use in the frontend UI."""
return self._icon
@property
def available(self) -> bool:
"""Return True if the device is currently available."""
return self._available
@property
def supported_features(self):
"""Get the list of supported features of the device."""
return self._supported_features
# These properties are common to ClimateDevice, WaterHeaterDevice classes
@property
def precision(self):
"""Return the temperature precision to use in the frontend UI."""
return PRECISION_HALVES
@property
def temperature_unit(self):
"""Return the temperature unit to use in the frontend UI."""
return TEMP_CELSIUS
@property
def operation_list(self):
"""Return the list of available operations."""
return self._operation_list

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