Merge commit 'df920b4eda1d64368ed3bf166bcb0a90aeec6c44' into rc
This commit is contained in:
commit
87d3680630
455 changed files with 16339 additions and 8596 deletions
10
.coveragerc
10
.coveragerc
|
@ -38,6 +38,8 @@ omit =
|
|||
homeassistant/components/apple_tv/*
|
||||
homeassistant/components/aqualogic/*
|
||||
homeassistant/components/aquostv/media_player.py
|
||||
homeassistant/components/arcam_fmj/media_player.py
|
||||
homeassistant/components/arcam_fmj/__init__.py
|
||||
homeassistant/components/arduino/*
|
||||
homeassistant/components/arest/binary_sensor.py
|
||||
homeassistant/components/arest/sensor.py
|
||||
|
@ -49,6 +51,7 @@ omit =
|
|||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/asuswrt/device_tracker.py
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/automatic/device_tracker.py
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/azure_event_hub/*
|
||||
|
@ -214,6 +217,7 @@ omit =
|
|||
homeassistant/components/fritzbox_callmonitor/sensor.py
|
||||
homeassistant/components/fritzbox_netmonitor/sensor.py
|
||||
homeassistant/components/fritzdect/switch.py
|
||||
homeassistant/components/fronius/sensor.py
|
||||
homeassistant/components/frontier_silicon/media_player.py
|
||||
homeassistant/components/futurenow/light.py
|
||||
homeassistant/components/garadget/cover.py
|
||||
|
@ -405,6 +409,8 @@ omit =
|
|||
homeassistant/components/nissan_leaf/*
|
||||
homeassistant/components/nmap_tracker/device_tracker.py
|
||||
homeassistant/components/nmbs/sensor.py
|
||||
homeassistant/components/notion/binary_sensor.py
|
||||
homeassistant/components/notion/sensor.py
|
||||
homeassistant/components/noaa_tides/sensor.py
|
||||
homeassistant/components/norway_air/air_quality.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.py
|
||||
|
@ -637,6 +643,7 @@ omit =
|
|||
homeassistant/components/trackr/device_tracker.py
|
||||
homeassistant/components/tradfri/*
|
||||
homeassistant/components/tradfri/light.py
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
homeassistant/components/transmission/*
|
||||
homeassistant/components/travisci/sensor.py
|
||||
|
@ -655,6 +662,7 @@ omit =
|
|||
homeassistant/components/uptimerobot/binary_sensor.py
|
||||
homeassistant/components/uscis/sensor.py
|
||||
homeassistant/components/usps/*
|
||||
homeassistant/components/vallox/*
|
||||
homeassistant/components/vasttrafik/sensor.py
|
||||
homeassistant/components/velbus/*
|
||||
homeassistant/components/velux/*
|
||||
|
@ -684,6 +692,8 @@ omit =
|
|||
homeassistant/components/worldtidesinfo/sensor.py
|
||||
homeassistant/components/worxlandroid/sensor.py
|
||||
homeassistant/components/wunderlist/*
|
||||
homeassistant/components/wwlln/__init__.py
|
||||
homeassistant/components/wwlln/geo_location.py
|
||||
homeassistant/components/x10/light.py
|
||||
homeassistant/components/xbox_live/sensor.py
|
||||
homeassistant/components/xeoma/camera.py
|
||||
|
|
17
.devcontainer/Dockerfile
Normal file
17
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM python:3.7
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libudev-dev libavformat-dev libavcodec-dev libavdevice-dev \
|
||||
libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install Python dependencies from requirements.txt if it exists
|
||||
COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspace/
|
||||
RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
24
.devcontainer/devcontainer.json
Normal file
24
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
{
|
||||
"name": "Home Assistant Dev",
|
||||
"context": "..",
|
||||
"dockerFile": "Dockerfile",
|
||||
"postCreateCommand": "pip3 install -e .",
|
||||
"appPort": 8123,
|
||||
"runArgs": [
|
||||
"-e", "GIT_EDTIOR='code --wait'"
|
||||
],
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-azure-devops.azure-pipelines",
|
||||
"redhat.vscode-yaml"
|
||||
],
|
||||
"settings": {
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.rulers": [80],
|
||||
"terminal.integrated.shell.linux": "/bin/bash"
|
||||
}
|
||||
}
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
|
@ -3,7 +3,7 @@
|
|||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- Do not report issues for integrations if you are using custom integration: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
|
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
|
@ -9,7 +9,7 @@ about: Create a report to help us improve
|
|||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- Do not report issues for integrations if you are using a custom integration: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
|
27
.github/lock.yml
vendored
Normal file
27
.github/lock.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 1
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2019-07-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
|
||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||
issues:
|
||||
daysUntilLock: 30
|
54
.github/stale.yml
vendored
Normal file
54
.github/stale.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- under investigation
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
There hasn't been any activity on this issue recently. Due to the high number
|
||||
of incoming GitHub notifications, we have to clean some of the old issues,
|
||||
as many of them have already been resolved with the latest updates.
|
||||
|
||||
Please make sure to update to the latest Home Assistant version and check
|
||||
if that solves the issue. Let us know if that works for you by adding a
|
||||
comment 👍
|
||||
|
||||
This issue now has been marked as stale and will be closed if no further
|
||||
activity occurs. Thank you for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
# closeComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -94,8 +94,10 @@ virtualization/vagrant/.vagrant
|
|||
virtualization/vagrant/config
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.devcontainer
|
||||
.vscode/*
|
||||
!.vscode/cSpell.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
# Built docs
|
||||
docs/build
|
||||
|
|
92
.vscode/tasks.json
vendored
Normal file
92
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Preview",
|
||||
"type": "shell",
|
||||
"command": "hass -c ./config",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Pytest",
|
||||
"type": "shell",
|
||||
"command": "pytest --timeout=10 tests",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Flake8",
|
||||
"type": "shell",
|
||||
"command": "flake8 homeassistant tests",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Pylint",
|
||||
"type": "shell",
|
||||
"command": "pylint homeassistant",
|
||||
"dependsOn": [
|
||||
"Install all Requirements"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Generate Requirements",
|
||||
"type": "shell",
|
||||
"command": "./script/gen_requirements_all.py",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
10
CODEOWNERS
10
CODEOWNERS
|
@ -26,9 +26,11 @@ homeassistant/components/ambiclimate/* @danielhiversen
|
|||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/aprs/* @PhilRW
|
||||
homeassistant/components/arcam_fmj/* @elupus
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automatic/* @armills
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
|
@ -91,6 +93,7 @@ homeassistant/components/flock/* @fabaff
|
|||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
|
@ -113,7 +116,6 @@ homeassistant/components/history/* @home-assistant/core
|
|||
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/honeywell/* @zxdavb
|
||||
|
@ -180,10 +182,12 @@ homeassistant/components/nissan_leaf/* @filcole
|
|||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
homeassistant/components/notify/* @home-assistant/core
|
||||
homeassistant/components/notion/* @bachya
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/opentherm_gw/* @mvn23
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
|
@ -237,6 +241,7 @@ homeassistant/components/spider/* @peternijssen
|
|||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/stream/* @hunterjm
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
|
@ -263,6 +268,7 @@ homeassistant/components/toon/* @frenck
|
|||
homeassistant/components/tplink/* @rytilahti
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/tts/* @robbiet480
|
||||
homeassistant/components/twilio_call/* @robbiet480
|
||||
homeassistant/components/twilio_sms/* @robbiet480
|
||||
|
@ -283,6 +289,7 @@ homeassistant/components/weblink/* @home-assistant/core
|
|||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/wwlln/* @bachya
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
|
@ -301,5 +308,4 @@ homeassistant/components/zoneminder/* @rohankapoorcom
|
|||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
|
||||
# Individual files
|
||||
homeassistant/components/group/cover @cdce8p
|
||||
homeassistant/components/demo/weather @fabaff
|
||||
|
|
|
@ -24,12 +24,14 @@ RUN virtualization/Docker/setup_docker_prereqs
|
|||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython tensorflow
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8123
|
||||
EXPOSE 8300
|
||||
EXPOSE 51827
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
|
|
|
@ -15,136 +15,163 @@ resources:
|
|||
image: homeassistant/ci-azure:3.6
|
||||
- container: 37
|
||||
image: homeassistant/ci-azure:3.7
|
||||
|
||||
|
||||
variables:
|
||||
- name: ArtifactFeed
|
||||
value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d'
|
||||
- name: PythonMain
|
||||
value: '35'
|
||||
|
||||
stages:
|
||||
|
||||
jobs:
|
||||
- stage: 'Overview'
|
||||
jobs:
|
||||
- job: 'Lint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
- job: 'Lint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv lint
|
||||
|
||||
. lint/bin/activate
|
||||
pip install flake8
|
||||
flake8 homeassistant tests script
|
||||
displayName: 'Run flake8'
|
||||
. venv/bin/activate
|
||||
pip install flake8
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
flake8 homeassistant tests script
|
||||
displayName: 'Run flake8'
|
||||
- job: 'Validate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest validate
|
||||
displayName: 'Validate manifests'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
./script/gen_requirements_all.py validate
|
||||
displayName: 'requirements_all validate'
|
||||
|
||||
- job: 'Check'
|
||||
- stage: 'Tests'
|
||||
dependsOn:
|
||||
- Lint
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 1
|
||||
matrix:
|
||||
Python35:
|
||||
python.version: '3.5'
|
||||
python.container: '35'
|
||||
Python36:
|
||||
python.version: '3.6'
|
||||
python.container: '36'
|
||||
Python37:
|
||||
python.version: '3.7'
|
||||
python.container: '37'
|
||||
container: $[ variables['python.container'] ]
|
||||
steps:
|
||||
- script: |
|
||||
echo "$(python.version)" > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'PyTest'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
Python35:
|
||||
python.container: '35'
|
||||
Python36:
|
||||
python.container: '36'
|
||||
Python37:
|
||||
python.container: '37'
|
||||
container: $[ variables['python.container'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python --version > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install pytest-azurepipelines -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 --junitxml=junit/test-results.xml -qq -o console_output_style=count -p no:sugar tests
|
||||
displayName: 'Run pytest for python $(python.version)'
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/test-*.xml'
|
||||
testRunTitle: 'Publish test results for Python $(python.version)'
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 --junitxml=junit/test-results.xml -qq -o console_output_style=count -p no:sugar tests
|
||||
displayName: 'Run pytest for python $(python.version)'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/test-*.xml'
|
||||
testRunTitle: 'Publish test results for Python $(python.version)'
|
||||
|
||||
- job: 'FullCheck'
|
||||
- stage: 'FullCheck'
|
||||
dependsOn:
|
||||
- Check
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
echo "$(PythonMain)" > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pylint homeassistant
|
||||
displayName: 'Run pylint'
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'Pytlint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python --version > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pylint homeassistant
|
||||
displayName: 'Run pylint'
|
||||
- job: 'Mypy'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
TYPING_FILES=$(cat mypyrc)
|
||||
mypy $TYPING_FILES
|
||||
displayName: 'Run mypy'
|
||||
|
|
|
@ -8,161 +8,162 @@ trigger:
|
|||
pr: none
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '4.5'
|
||||
value: '5.1'
|
||||
- group: docker
|
||||
- group: github
|
||||
- group: twine
|
||||
|
||||
|
||||
jobs:
|
||||
stages:
|
||||
|
||||
- stage: 'Validate'
|
||||
jobs:
|
||||
- job: 'VersionValidate'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: |
|
||||
setup_version="$(python setup.py -V)"
|
||||
branch_version="$(Build.SourceBranchName)"
|
||||
|
||||
- job: 'VersionValidate'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: |
|
||||
setup_version="$(python setup.py -V)"
|
||||
branch_version="$(Build.SourceBranchName)"
|
||||
if [ "${setup_version}" != "${branch_version}" ]; then
|
||||
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
|
||||
exit 1
|
||||
fi
|
||||
displayName: 'Check version of branch/tag'
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
jq curl
|
||||
|
||||
if [ "${setup_version}" != "${branch_version}" ]; then
|
||||
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
|
||||
release="$(Build.SourceBranchName)"
|
||||
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
|
||||
|
||||
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${created_by} is not allowed to create an release!"
|
||||
exit 1
|
||||
fi
|
||||
displayName: 'Check version of branch/tag'
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
jq curl
|
||||
displayName: 'Check rights'
|
||||
|
||||
release="$(Build.SourceBranchName)"
|
||||
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
|
||||
- stage: 'Build'
|
||||
jobs:
|
||||
- job: 'ReleasePython'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: pip install twine wheel
|
||||
displayName: 'Install tools'
|
||||
- script: python setup.py sdist bdist_wheel
|
||||
displayName: 'Build package'
|
||||
- script: |
|
||||
export TWINE_USERNAME="$(twineUser)"
|
||||
export TWINE_PASSWORD="$(twinePassword)"
|
||||
|
||||
twine upload dist/* --skip-existing
|
||||
displayName: 'Upload pypi'
|
||||
- job: 'ReleaseDocker'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
timeoutInMinutes: 240
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
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
|
||||
|
||||
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
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
|
||||
|
||||
echo "${created_by} is not allowed to create an release!"
|
||||
exit 1
|
||||
displayName: 'Check rights'
|
||||
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'
|
||||
|
||||
- stage: 'Publish'
|
||||
jobs:
|
||||
- job: 'ReleaseHassio'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker'))
|
||||
dependsOn:
|
||||
- 'ReleaseDocker'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git jq curl
|
||||
|
||||
- job: 'ReleasePython'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: pip install twine wheel
|
||||
displayName: 'Install tools'
|
||||
- script: python setup.py sdist bdist_wheel
|
||||
displayName: 'Build package'
|
||||
- script: |
|
||||
export TWINE_USERNAME="$(twineUser)"
|
||||
export TWINE_PASSWORD="$(twinePassword)"
|
||||
|
||||
twine upload dist/* --skip-existing
|
||||
displayName: 'Upload pypi'
|
||||
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
|
||||
|
||||
- job: 'ReleaseDocker'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
timeoutInMinutes: 240
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
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
|
||||
version="$(Build.SourceBranchName)"
|
||||
|
||||
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
|
||||
git clone https://github.com/home-assistant/hassio-version
|
||||
cd hassio-version
|
||||
|
||||
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'
|
||||
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
|
||||
|
||||
- job: 'ReleaseHassio'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker'))
|
||||
dependsOn:
|
||||
- 'ReleaseDocker'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git jq curl
|
||||
|
||||
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
|
||||
displayName: 'Update version files'
|
||||
git commit -am "Bump Home Assistant $version"
|
||||
git push
|
||||
displayName: 'Update version files'
|
||||
|
|
|
@ -547,7 +547,7 @@ class AuthStore:
|
|||
|
||||
def _set_defaults(self) -> None:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
self._users = OrderedDict()
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
admin_group = _system_admin_group()
|
||||
|
|
|
@ -36,7 +36,7 @@ def is_on(hass, entity_id=None):
|
|||
continue
|
||||
|
||||
if not hasattr(component, 'is_on'):
|
||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||
_LOGGER.warning("Integration %s has no is_on method.", domain)
|
||||
continue
|
||||
|
||||
if component.is_on(ent_id):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.",
|
||||
"single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern.",
|
||||
"title": "Verkn\u00fcpfe AdGuard Home."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Updated existing configuration.",
|
||||
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
|
@ -16,7 +17,7 @@
|
|||
"host": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"port": "\ud3ec\ud2b8",
|
||||
"ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4",
|
||||
"ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
|
||||
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
|
||||
"verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.",
|
||||
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Bestaande configuratie bijgewerkt.",
|
||||
"single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
|
||||
"single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.",
|
||||
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada.",
|
||||
"single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
|
||||
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.",
|
||||
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002",
|
||||
"single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002"
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -4,5 +4,8 @@
|
|||
"documentation": "https://www.home-assistant.io/components/alert",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"after_dependencies": [
|
||||
"notify"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import homeassistant.util.color as color_util
|
|||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
)
|
||||
|
@ -180,9 +181,13 @@ class AlexaPowerController(AlexaCapibility):
|
|||
if name != 'powerState':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_OFF:
|
||||
return 'OFF'
|
||||
return 'ON'
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
||||
|
||||
else:
|
||||
is_on = self.entity.state != STATE_OFF
|
||||
|
||||
return 'ON' if is_on else 'OFF'
|
||||
|
||||
|
||||
class AlexaLockController(AlexaCapibility):
|
||||
|
@ -546,16 +551,13 @@ class AlexaThermostatController(AlexaCapibility):
|
|||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
properties = []
|
||||
properties = [{'name': 'thermostatMode'}]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||
properties.append({'name': 'targetSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE:
|
||||
properties.append({'name': 'lowerSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||
properties.append({'name': 'upperSetpoint'})
|
||||
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||
properties.append({'name': 'thermostatMode'})
|
||||
return properties
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
|
@ -569,13 +571,18 @@ class AlexaThermostatController(AlexaCapibility):
|
|||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name == 'thermostatMode':
|
||||
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||
if mode is None:
|
||||
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||
raise UnsupportedProperty(name)
|
||||
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
|
||||
|
||||
if preset in API_THERMOSTAT_PRESETS:
|
||||
mode = API_THERMOSTAT_PRESETS[preset]
|
||||
else:
|
||||
mode = API_THERMOSTAT_MODES.get(self.entity.state)
|
||||
if mode is None:
|
||||
_LOGGER.error(
|
||||
"%s (%s) has unsupported state value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
self.entity.state)
|
||||
raise UnsupportedProperty(name)
|
||||
return mode
|
||||
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
|
@ -57,16 +56,17 @@ API_TEMP_UNITS = {
|
|||
# reverse mapping of this dict and we want to map the first occurrance of OFF
|
||||
# back to HA state.
|
||||
API_THERMOSTAT_MODES = OrderedDict([
|
||||
(climate.STATE_HEAT, 'HEAT'),
|
||||
(climate.STATE_COOL, 'COOL'),
|
||||
(climate.STATE_AUTO, 'AUTO'),
|
||||
(climate.STATE_ECO, 'ECO'),
|
||||
(climate.STATE_MANUAL, 'AUTO'),
|
||||
(STATE_OFF, 'OFF'),
|
||||
(climate.STATE_IDLE, 'OFF'),
|
||||
(climate.STATE_FAN_ONLY, 'OFF'),
|
||||
(climate.STATE_DRY, 'OFF'),
|
||||
(climate.HVAC_MODE_HEAT, 'HEAT'),
|
||||
(climate.HVAC_MODE_COOL, 'COOL'),
|
||||
(climate.HVAC_MODE_HEAT_COOL, 'AUTO'),
|
||||
(climate.HVAC_MODE_AUTO, 'AUTO'),
|
||||
(climate.HVAC_MODE_OFF, 'OFF'),
|
||||
(climate.HVAC_MODE_FAN_ONLY, 'OFF'),
|
||||
(climate.HVAC_MODE_DRY, 'OFF'),
|
||||
])
|
||||
API_THERMOSTAT_PRESETS = {
|
||||
climate.PRESET_ECO: 'ECO'
|
||||
}
|
||||
|
||||
PERCENTAGE_FAN_MAP = {
|
||||
fan.SPEED_LOW: 33,
|
||||
|
|
|
@ -248,9 +248,11 @@ class ClimateCapabilities(AlexaEntity):
|
|||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_ON_OFF:
|
||||
# If we support two modes, one being off, we allow turning on too.
|
||||
if len([v for v in self.entity.attributes[climate.ATTR_HVAC_MODES]
|
||||
if v != climate.HVAC_MODE_OFF]) == 1:
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
yield AlexaThermostatController(self.hass, self.entity)
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
|
|
@ -33,6 +33,7 @@ from homeassistant.util.temperature import convert as convert_temperature
|
|||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
Cause,
|
||||
)
|
||||
from .entities import async_get_entities
|
||||
|
@ -686,23 +687,45 @@ async def async_api_set_thermostat_mode(hass, config, directive, context):
|
|||
mode = directive.payload['thermostatMode']
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(mode)
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||
}
|
||||
|
||||
ha_preset = next(
|
||||
(k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode),
|
||||
None
|
||||
)
|
||||
|
||||
if ha_preset:
|
||||
presets = entity.attributes.get(climate.ATTR_PRESET_MODES, [])
|
||||
|
||||
if ha_preset not in presets:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(
|
||||
ha_preset
|
||||
)
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
|
||||
service = climate.SERVICE_SET_PRESET_MODE
|
||||
data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO
|
||||
|
||||
else:
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(
|
||||
mode
|
||||
)
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
|
||||
service = climate.SERVICE_SET_HVAC_MODE
|
||||
data[climate.ATTR_HVAC_MODE] = ha_mode
|
||||
|
||||
response = directive.response()
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||
climate.DOMAIN, service, data,
|
||||
blocking=False, context=context)
|
||||
response.add_context_property({
|
||||
'name': 'thermostatMode',
|
||||
|
|
|
@ -7,11 +7,8 @@ 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)
|
||||
SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, HVAC_MODE_HEAT)
|
||||
from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, 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,
|
||||
|
@ -20,8 +17,7 @@ from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_ON_OFF)
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
|
@ -177,11 +173,6 @@ class AmbiclimateEntity(ClimateDevice):
|
|||
"""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."""
|
||||
|
@ -198,9 +189,17 @@ class AmbiclimateEntity(ClimateDevice):
|
|||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available hvac operation modes."""
|
||||
return [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT if self.is_on else STATE_OFF
|
||||
if self._data.get('power', '').lower() == 'on':
|
||||
return HVAC_MODE_HEAT
|
||||
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
@ -209,13 +208,13 @@ class AmbiclimateEntity(ClimateDevice):
|
|||
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_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
await self._heater.turn_on()
|
||||
return
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
await self._heater.turn_off()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
|
|
|
@ -119,8 +119,8 @@ TYPE_WINDSPEEDMPH = 'windspeedmph'
|
|||
TYPE_YEARLYRAININ = 'yearlyrainin'
|
||||
SENSOR_TYPES = {
|
||||
TYPE_24HOURRAININ: ('24 Hr Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, None),
|
||||
TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, None),
|
||||
TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
|
||||
TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
|
||||
TYPE_BATT10: ('Battery 10', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_BATT1: ('Battery 1', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_BATT2: ('Battery 2', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
|
@ -134,23 +134,23 @@ SENSOR_TYPES = {
|
|||
TYPE_BATTOUT: ('Battery', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_CO2: ('co2', 'ppm', TYPE_SENSOR, None),
|
||||
TYPE_DAILYRAININ: ('Daily Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, None),
|
||||
TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_EVENTRAININ: ('Event Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, None),
|
||||
TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_HOURLYRAININ: ('Hourly Rain Rate', 'in/hr', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, None),
|
||||
TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, 'timestamp'),
|
||||
TYPE_MAXDAILYGUST: ('Max Gust', 'mph', TYPE_SENSOR, None),
|
||||
TYPE_MONTHLYRAININ: ('Monthly Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_RELAY10: ('Relay 10', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
|
@ -163,39 +163,39 @@ SENSOR_TYPES = {
|
|||
TYPE_RELAY7: ('Relay 7', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_RELAY8: ('Relay 8', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_RELAY9: ('Relay 9', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOLARRADIATION: ('Solar Rad', 'W/m^2', TYPE_SENSOR, None),
|
||||
TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOLARRADIATION: ('Solar Rad', 'lx', TYPE_SENSOR, 'illuminance'),
|
||||
TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TOTALRAININ: ('Lifetime Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_UV: ('uv', 'Index', TYPE_SENSOR, None),
|
||||
TYPE_WEEKLYRAININ: ('Weekly Rain', 'in', TYPE_SENSOR, None),
|
||||
|
@ -404,9 +404,10 @@ class AmbientWeatherEntity(Entity):
|
|||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type,
|
||||
sensor_name):
|
||||
sensor_name, device_class):
|
||||
"""Initialize the sensor."""
|
||||
self._ambient = ambient
|
||||
self._device_class = device_class
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._mac_address = mac_address
|
||||
self._sensor_name = sensor_name
|
||||
|
@ -420,6 +421,11 @@ class AmbientWeatherEntity(Entity):
|
|||
return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._sensor_type) is not None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device registry information for this entity."""
|
||||
|
|
|
@ -39,20 +39,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice):
|
||||
"""Define an Ambient binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type, sensor_name,
|
||||
device_class):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
ambient, mac_address, station_name, sensor_type, sensor_name)
|
||||
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
|
||||
from . import SENSOR_TYPES, AmbientWeatherEntity
|
||||
from . import SENSOR_TYPES, TYPE_SOLARRADIATION, AmbientWeatherEntity
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -22,12 +22,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in ambient.monitored_conditions:
|
||||
name, unit, kind, _ = SENSOR_TYPES[condition]
|
||||
name, unit, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == TYPE_SENSOR:
|
||||
sensor_list.append(
|
||||
AmbientWeatherSensor(
|
||||
ambient, mac_address, station[ATTR_NAME], condition,
|
||||
name, unit))
|
||||
name, device_class, unit))
|
||||
|
||||
async_add_entities(sensor_list, True)
|
||||
|
||||
|
@ -37,10 +37,15 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
|
|||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type, sensor_name,
|
||||
unit):
|
||||
device_class, unit):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
ambient, mac_address, station_name, sensor_type, sensor_name)
|
||||
ambient,
|
||||
mac_address,
|
||||
station_name,
|
||||
sensor_type,
|
||||
sensor_name,
|
||||
device_class)
|
||||
|
||||
self._unit = unit
|
||||
|
||||
|
@ -56,5 +61,13 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._state = self._ambient.stations[
|
||||
new_state = self._ambient.stations[
|
||||
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
|
||||
|
||||
if self._sensor_type == TYPE_SOLARRADIATION:
|
||||
# Ambient's units for solar radiation (illuminance) are
|
||||
# W/m^2; since those aren't commonly used in the HASS
|
||||
# world, transform them to lx:
|
||||
self._state = round(float(new_state)/0.0079)
|
||||
else:
|
||||
self._state = new_state
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Androidtv",
|
||||
"documentation": "https://www.home-assistant.io/components/androidtv",
|
||||
"requirements": [
|
||||
"androidtv==0.0.16"
|
||||
"androidtv==0.0.18"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
|
5
homeassistant/components/arcam_fmj/.translations/ca.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/ca.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
5
homeassistant/components/arcam_fmj/.translations/en.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/en.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
5
homeassistant/components/arcam_fmj/.translations/ko.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/ko.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
17
homeassistant/components/arcam_fmj/.translations/nl.json
Normal file
17
homeassistant/components/arcam_fmj/.translations/nl.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"error": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"step": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
23
homeassistant/components/arcam_fmj/.translations/pl.json
Normal file
23
homeassistant/components/arcam_fmj/.translations/pl.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"error": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"step": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
176
homeassistant/components/arcam_fmj/__init__.py
Normal file
176
homeassistant/components/arcam_fmj/__init__.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
"""Arcam component."""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
import async_timeout
|
||||
from arcam.fmj.client import Client
|
||||
from arcam.fmj import ConnectionFailed
|
||||
|
||||
from homeassistant import config_entries
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_ZONE,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DOMAIN_DATA_ENTRIES,
|
||||
DOMAIN_DATA_CONFIG,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
SIGNAL_CLIENT_DATA,
|
||||
SIGNAL_CLIENT_STARTED,
|
||||
SIGNAL_CLIENT_STOPPED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _optional_zone(value):
|
||||
if value:
|
||||
return ZONE_SCHEMA(value)
|
||||
return ZONE_SCHEMA({})
|
||||
|
||||
|
||||
def _zone_name_validator(config):
|
||||
for zone, zone_config in config[CONF_ZONE].items():
|
||||
if CONF_NAME not in zone_config:
|
||||
zone_config[CONF_NAME] = "{} ({}:{}) - {}".format(
|
||||
DEFAULT_NAME,
|
||||
config[CONF_HOST],
|
||||
config[CONF_PORT],
|
||||
zone)
|
||||
return config
|
||||
|
||||
|
||||
ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(SERVICE_TURN_ON): cv.SERVICE_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema(
|
||||
vol.All({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_ZONE, default={1: _optional_zone(None)}
|
||||
): {vol.In([1, 2]): _optional_zone},
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
||||
): cv.positive_int,
|
||||
}, _zone_name_validator)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the component."""
|
||||
hass.data[DOMAIN_DATA_ENTRIES] = {}
|
||||
hass.data[DOMAIN_DATA_CONFIG] = {}
|
||||
|
||||
for device in config[DOMAIN]:
|
||||
hass.data[DOMAIN_DATA_CONFIG][
|
||||
(device[CONF_HOST], device[CONF_PORT])
|
||||
] = device
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_HOST: device[CONF_HOST],
|
||||
CONF_PORT: device[CONF_PORT],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Set up an access point from a config entry."""
|
||||
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
|
||||
config = hass.data[DOMAIN_DATA_CONFIG].get(
|
||||
(entry.data[CONF_HOST], entry.data[CONF_PORT]),
|
||||
DEVICE_SCHEMA(
|
||||
{
|
||||
CONF_HOST: entry.data[CONF_HOST],
|
||||
CONF_PORT: entry.data[CONF_PORT],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
|
||||
"client": client,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
asyncio.ensure_future(
|
||||
_run_client(hass, client, config[CONF_SCAN_INTERVAL])
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _run_client(hass, client, interval):
|
||||
task = asyncio.Task.current_task()
|
||||
run = True
|
||||
|
||||
async def _stop(_):
|
||||
nonlocal run
|
||||
run = False
|
||||
task.cancel()
|
||||
await task
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop)
|
||||
|
||||
def _listen(_):
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_DATA, client.host
|
||||
)
|
||||
|
||||
while run:
|
||||
try:
|
||||
with async_timeout.timeout(interval):
|
||||
await client.start()
|
||||
|
||||
_LOGGER.debug("Client connected %s", client.host)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_STARTED, client.host
|
||||
)
|
||||
|
||||
try:
|
||||
with client.listen(_listen):
|
||||
await client.process()
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
_LOGGER.debug("Client disconnected %s", client.host)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_STOPPED, client.host
|
||||
)
|
||||
|
||||
except ConnectionFailed:
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
27
homeassistant/components/arcam_fmj/config_flow.py
Normal file
27
homeassistant/components/arcam_fmj/config_flow.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Config flow to configure the Arcam FMJ component."""
|
||||
from operator import itemgetter
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_GETKEY = itemgetter(CONF_HOST, CONF_PORT)
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a SimpliSafe config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
entries = self.hass.config_entries.async_entries(DOMAIN)
|
||||
import_key = _GETKEY(import_config)
|
||||
for entry in entries:
|
||||
if _GETKEY(entry.data) == import_key:
|
||||
return self.async_abort(reason="already_setup")
|
||||
|
||||
return self.async_create_entry(title="Arcam FMJ", data=import_config)
|
13
homeassistant/components/arcam_fmj/const.py
Normal file
13
homeassistant/components/arcam_fmj/const.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""Constants used for arcam."""
|
||||
DOMAIN = "arcam_fmj"
|
||||
|
||||
SIGNAL_CLIENT_STARTED = "arcam.client_started"
|
||||
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
|
||||
SIGNAL_CLIENT_DATA = "arcam.client_data"
|
||||
|
||||
DEFAULT_PORT = 50000
|
||||
DEFAULT_NAME = "Arcam FMJ"
|
||||
DEFAULT_SCAN_INTERVAL = 5
|
||||
|
||||
DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN)
|
||||
DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN)
|
13
homeassistant/components/arcam_fmj/manifest.json
Normal file
13
homeassistant/components/arcam_fmj/manifest.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"domain": "arcam_fmj",
|
||||
"name": "Arcam FMJ Receiver control",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/components/arcam_fmj",
|
||||
"requirements": [
|
||||
"arcam-fmj==0.4.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@elupus"
|
||||
]
|
||||
}
|
342
homeassistant/components/arcam_fmj/media_player.py
Normal file
342
homeassistant/components/arcam_fmj/media_player.py
Normal file
|
@ -0,0 +1,342 @@
|
|||
"""Arcam media player."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from arcam.fmj import (
|
||||
DecodeMode2CH,
|
||||
DecodeModeMCH,
|
||||
IncomingAudioFormat,
|
||||
SourceCodes,
|
||||
)
|
||||
from arcam.fmj.state import State
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.media_player import MediaPlayerDevice
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_STEP,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_ZONE,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.helpers.service import async_call_from_config
|
||||
|
||||
from .const import (
|
||||
SIGNAL_CLIENT_DATA,
|
||||
SIGNAL_CLIENT_STARTED,
|
||||
SIGNAL_CLIENT_STOPPED,
|
||||
DOMAIN_DATA_ENTRIES,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
async_add_entities,
|
||||
):
|
||||
"""Set up the configuration entry."""
|
||||
data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id]
|
||||
client = data["client"]
|
||||
config = data["config"]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ArcamFmj(
|
||||
State(client, zone),
|
||||
zone_config[CONF_NAME],
|
||||
zone_config.get(SERVICE_TURN_ON),
|
||||
)
|
||||
for zone, zone_config in config[CONF_ZONE].items()
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ArcamFmj(MediaPlayerDevice):
|
||||
"""Representation of a media device."""
|
||||
|
||||
def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]):
|
||||
"""Initialize device."""
|
||||
self._state = state
|
||||
self._name = name
|
||||
self._turn_on = turn_on
|
||||
self._support = (
|
||||
SUPPORT_SELECT_SOURCE
|
||||
| SUPPORT_VOLUME_SET
|
||||
| SUPPORT_VOLUME_MUTE
|
||||
| SUPPORT_VOLUME_STEP
|
||||
| SUPPORT_TURN_OFF
|
||||
)
|
||||
if state.zn == 1:
|
||||
self._support |= SUPPORT_SELECT_SOUND_MODE
|
||||
|
||||
def _get_2ch(self):
|
||||
"""Return if source is 2 channel or not."""
|
||||
audio_format, _ = self._state.get_incoming_audio_format()
|
||||
return bool(
|
||||
audio_format
|
||||
in (
|
||||
IncomingAudioFormat.PCM,
|
||||
IncomingAudioFormat.ANALOGUE_DIRECT,
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
"identifiers": {
|
||||
(DOMAIN, self._state.client.host, self._state.client.port)
|
||||
},
|
||||
"model": "FMJ",
|
||||
"manufacturer": "Arcam",
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No need to poll."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the controlled device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state.get_power():
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
support = self._support
|
||||
if self._state.get_power() is not None or self._turn_on:
|
||||
support |= SUPPORT_TURN_ON
|
||||
return support
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Once registed add listener for events."""
|
||||
await self._state.start()
|
||||
|
||||
@callback
|
||||
def _data(host):
|
||||
if host == self._state.client.host:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _started(host):
|
||||
if host == self._state.client.host:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
@callback
|
||||
def _stopped(host):
|
||||
if host == self._state.client.host:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_CLIENT_DATA, _data
|
||||
)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_CLIENT_STARTED, _started
|
||||
)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_CLIENT_STOPPED, _stopped
|
||||
)
|
||||
|
||||
async def async_update(self):
|
||||
"""Force update of state."""
|
||||
_LOGGER.debug("Update state %s", self.name)
|
||||
await self._state.update()
|
||||
|
||||
async def async_mute_volume(self, mute):
|
||||
"""Send mute command."""
|
||||
await self._state.set_mute(mute)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_select_source(self, source):
|
||||
"""Select a specific source."""
|
||||
try:
|
||||
value = SourceCodes[source]
|
||||
except KeyError:
|
||||
_LOGGER.error("Unsupported source %s", source)
|
||||
return
|
||||
|
||||
await self._state.set_source(value)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_select_sound_mode(self, sound_mode):
|
||||
"""Select a specific source."""
|
||||
try:
|
||||
if self._get_2ch():
|
||||
await self._state.set_decode_mode_2ch(
|
||||
DecodeMode2CH[sound_mode]
|
||||
)
|
||||
else:
|
||||
await self._state.set_decode_mode_mch(
|
||||
DecodeModeMCH[sound_mode]
|
||||
)
|
||||
except KeyError:
|
||||
_LOGGER.error("Unsupported sound_mode %s", sound_mode)
|
||||
return
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
await self._state.set_volume(round(volume * 99.0))
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_volume_up(self):
|
||||
"""Turn volume up for media player."""
|
||||
await self._state.inc_volume()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_volume_down(self):
|
||||
"""Turn volume up for media player."""
|
||||
await self._state.dec_volume()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
if self._state.get_power() is not None:
|
||||
_LOGGER.debug("Turning on device using connection")
|
||||
await self._state.set_power(True)
|
||||
elif self._turn_on:
|
||||
_LOGGER.debug("Turning on device using service call")
|
||||
await async_call_from_config(
|
||||
self.hass,
|
||||
self._turn_on,
|
||||
variables=None,
|
||||
blocking=True,
|
||||
validate_config=False,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Unable to turn on")
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
await self._state.set_power(False)
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
value = self._state.get_source()
|
||||
if value is None:
|
||||
return None
|
||||
return value.name
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return [x.name for x in self._state.get_source_list()]
|
||||
|
||||
@property
|
||||
def sound_mode(self):
|
||||
"""Name of the current sound mode."""
|
||||
if self._state.zn != 1:
|
||||
return None
|
||||
|
||||
if self._get_2ch():
|
||||
value = self._state.get_decode_mode_2ch()
|
||||
else:
|
||||
value = self._state.get_decode_mode_mch()
|
||||
if value:
|
||||
return value.name
|
||||
return None
|
||||
|
||||
@property
|
||||
def sound_mode_list(self):
|
||||
"""List of available sound modes."""
|
||||
if self._state.zn != 1:
|
||||
return None
|
||||
|
||||
if self._get_2ch():
|
||||
return [x.name for x in DecodeMode2CH]
|
||||
return [x.name for x in DecodeModeMCH]
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
value = self._state.get_mute()
|
||||
if value is None:
|
||||
return None
|
||||
return value
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of device."""
|
||||
value = self._state.get_volume()
|
||||
if value is None:
|
||||
return None
|
||||
return value / 99.0
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
source = self._state.get_source()
|
||||
if source == SourceCodes.DAB:
|
||||
value = MEDIA_TYPE_MUSIC
|
||||
elif source == SourceCodes.FM:
|
||||
value = MEDIA_TYPE_MUSIC
|
||||
else:
|
||||
value = None
|
||||
return value
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
"""Channel currently playing."""
|
||||
source = self._state.get_source()
|
||||
if source == SourceCodes.DAB:
|
||||
value = self._state.get_dab_station()
|
||||
elif source == SourceCodes.FM:
|
||||
value = self._state.get_rds_information()
|
||||
else:
|
||||
value = None
|
||||
return value
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
source = self._state.get_source()
|
||||
if source == SourceCodes.DAB:
|
||||
value = self._state.get_dls_pdt()
|
||||
else:
|
||||
value = None
|
||||
return value
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
source = self._state.get_source()
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
channel = self.media_channel
|
||||
|
||||
if channel:
|
||||
value = "{} - {}".format(source.name, channel)
|
||||
else:
|
||||
value = source.name
|
||||
return value
|
8
homeassistant/components/arcam_fmj/strings.json
Normal file
8
homeassistant/components/arcam_fmj/strings.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ",
|
||||
"step": {},
|
||||
"error": {},
|
||||
"abort": {}
|
||||
}
|
||||
}
|
1
homeassistant/components/aurora_abb_powerone/__init__.py
Normal file
1
homeassistant/components/aurora_abb_powerone/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""The Aurora ABB Powerone PV inverter sensor integration."""
|
10
homeassistant/components/aurora_abb_powerone/manifest.json
Normal file
10
homeassistant/components/aurora_abb_powerone/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "aurora_abb_powerone",
|
||||
"name": "Aurora ABB Solar PV",
|
||||
"documentation": "https://www.home-assistant.io/components/aurora_abb_powerone/",
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@davet2001"
|
||||
],
|
||||
"requirements": ["aurorapy==0.2.6"]
|
||||
}
|
98
homeassistant/components/aurora_abb_powerone/sensor.py
Normal file
98
homeassistant/components/aurora_abb_powerone/sensor.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
"""Support for Aurora ABB PowerOne Solar Photvoltaic (PV) inverter."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from aurorapy.client import AuroraSerialClient, AuroraError
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS, CONF_DEVICE, CONF_NAME, DEVICE_CLASS_POWER,
|
||||
POWER_WATT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ADDRESS = 2
|
||||
DEFAULT_NAME = "Solar PV"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Aurora ABB PowerOne device."""
|
||||
devices = []
|
||||
comport = config[CONF_DEVICE]
|
||||
address = config[CONF_ADDRESS]
|
||||
name = config[CONF_NAME]
|
||||
|
||||
_LOGGER.debug("Intitialising com port=%s address=%s", comport, address)
|
||||
client = AuroraSerialClient(address, comport, parity='N', timeout=1)
|
||||
|
||||
devices.append(AuroraABBSolarPVMonitorSensor(client, name, 'Power'))
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class AuroraABBSolarPVMonitorSensor(Entity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
def __init__(self, client, name, typename):
|
||||
"""Initialize the sensor."""
|
||||
self._name = "{} {}".format(name, typename)
|
||||
self.client = client
|
||||
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."""
|
||||
return POWER_WATT
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return DEVICE_CLASS_POWER
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for the sensor.
|
||||
|
||||
This is the only method that should fetch new data for Home Assistant.
|
||||
"""
|
||||
try:
|
||||
self.client.connect()
|
||||
# read ADC channel 3 (grid power output)
|
||||
power_watts = self.client.measure(3, True)
|
||||
self._state = round(power_watts, 1)
|
||||
# _LOGGER.debug("Got reading %fW" % self._state)
|
||||
except AuroraError as error:
|
||||
# aurorapy does not have different exceptions (yet) for dealing
|
||||
# with timeout vs other comms errors.
|
||||
# This means the (normal) situation of no response during darkness
|
||||
# raises an exception.
|
||||
# aurorapy (gitlab) pull request merged 29/5/2019. When >0.2.6 is
|
||||
# released, this could be modified to :
|
||||
# except AuroraTimeoutError as e:
|
||||
# Workaround: look at the text of the exception
|
||||
if "No response after" in str(error):
|
||||
_LOGGER.debug("No response from inverter (could be dark)")
|
||||
else:
|
||||
# print("Exception!!: {}".format(str(e)))
|
||||
raise error
|
||||
self._state = None
|
||||
finally:
|
||||
if self.client.serline.isOpen():
|
||||
self.client.close()
|
|
@ -18,7 +18,7 @@ from homeassistant.helpers.entity import ToggleEntity
|
|||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.dt import parse_datetime, utcnow
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
@ -227,7 +227,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
last_triggered = state.attributes.get('last_triggered')
|
||||
if last_triggered is not None:
|
||||
self._last_triggered = parse_datetime(last_triggered)
|
||||
_LOGGER.debug("Loaded automation %s with state %s from state "
|
||||
" storage last state %s", self.entity_id,
|
||||
enable_automation, state)
|
||||
|
|
|
@ -3,13 +3,14 @@ import logging
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
from homeassistant.helpers import condition, config_validation as cv, template
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
|
@ -17,7 +18,9 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_FOR): vol.Any(
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
cv.template, cv.template_complex),
|
||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -29,9 +32,11 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
template.attach(hass, time_delta)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
unsub_track_same = {}
|
||||
entities_triggered = set()
|
||||
period = {}
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
@ -67,6 +72,7 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'for': time_delta if not time_delta else period[entity],
|
||||
}
|
||||
}, context=to_s.context))
|
||||
|
||||
|
@ -78,8 +84,39 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
entities_triggered.add(entity)
|
||||
|
||||
if time_delta:
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
if isinstance(time_delta, template.Template):
|
||||
period[entity] = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta.async_render(variables))
|
||||
elif isinstance(time_delta, dict):
|
||||
time_delta_data = {}
|
||||
time_delta_data.update(
|
||||
template.render_complex(time_delta, variables))
|
||||
period[entity] = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta_data)
|
||||
else:
|
||||
period[entity] = time_delta
|
||||
except (exceptions.TemplateError, vol.Invalid) as ex:
|
||||
_LOGGER.error("Error rendering '%s' for template: %s",
|
||||
automation_info['name'], ex)
|
||||
entities_triggered.discard(entity)
|
||||
return
|
||||
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
hass, period[entity], call_action, entity_ids=entity,
|
||||
async_check_same_func=check_numeric_state)
|
||||
else:
|
||||
call_action()
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
"""Offer state listening automation rules."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
CONF_FROM = 'from'
|
||||
|
@ -17,7 +22,9 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
# These are str on purpose. Want to catch YAML conversions
|
||||
vol.Optional(CONF_FROM): str,
|
||||
vol.Optional(CONF_TO): str,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_FOR): vol.Any(
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
cv.template, cv.template_complex),
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
|
||||
|
||||
|
@ -27,8 +34,10 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
template.attach(hass, time_delta)
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
unsub_track_same = {}
|
||||
period = {}
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
|
@ -42,7 +51,7 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'for': time_delta,
|
||||
'for': time_delta if not time_delta else period[entity]
|
||||
}
|
||||
}, context=to_s.context))
|
||||
|
||||
|
@ -55,10 +64,40 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
call_action()
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
if isinstance(time_delta, template.Template):
|
||||
period[entity] = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta.async_render(variables))
|
||||
elif isinstance(time_delta, dict):
|
||||
time_delta_data = {}
|
||||
time_delta_data.update(
|
||||
template.render_complex(time_delta, variables))
|
||||
period[entity] = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta_data)
|
||||
else:
|
||||
period[entity] = time_delta
|
||||
except (exceptions.TemplateError, vol.Invalid) as ex:
|
||||
_LOGGER.error("Error rendering '%s' for template: %s",
|
||||
automation_info['name'], ex)
|
||||
return
|
||||
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action,
|
||||
hass, period[entity], call_action,
|
||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||
entity_ids=entity_id)
|
||||
entity_ids=entity)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
|
|
@ -5,17 +5,20 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_FOR
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_same_state, async_track_template)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'template',
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_FOR): vol.Any(
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
cv.template, cv.template_complex),
|
||||
})
|
||||
|
||||
|
||||
|
@ -24,6 +27,7 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
time_delta = config.get(CONF_FOR)
|
||||
template.attach(hass, time_delta)
|
||||
unsub_track_same = None
|
||||
|
||||
@callback
|
||||
|
@ -40,6 +44,7 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'for': time_delta if not time_delta else period
|
||||
},
|
||||
}, context=(to_s.context if to_s else None)))
|
||||
|
||||
|
@ -47,8 +52,38 @@ async def async_trigger(hass, config, action, automation_info):
|
|||
call_action()
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
if isinstance(time_delta, template.Template):
|
||||
period = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta.async_render(variables))
|
||||
elif isinstance(time_delta, dict):
|
||||
time_delta_data = {}
|
||||
time_delta_data.update(
|
||||
template.render_complex(time_delta, variables))
|
||||
period = vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta)(
|
||||
time_delta_data)
|
||||
else:
|
||||
period = time_delta
|
||||
except (exceptions.TemplateError, vol.Invalid) as ex:
|
||||
_LOGGER.error("Error rendering '%s' for template: %s",
|
||||
automation_info['name'], ex)
|
||||
return
|
||||
|
||||
unsub_track_same = async_track_same_state(
|
||||
hass, time_delta, call_action,
|
||||
hass, period, call_action,
|
||||
lambda _, _2, _3: condition.async_template(hass, value_template),
|
||||
value_template.extract_entities())
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"abort": {
|
||||
"already_configured": "Enheten er allerede konfigurert",
|
||||
"bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen",
|
||||
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke"
|
||||
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
|
||||
"not_axis_device": "Oppdaget enhet ikke en Axis enhet"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Enheten er allerede konfigurert",
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"abort": {
|
||||
"already_configured": "Naprava je \u017ee konfigurirana",
|
||||
"bad_config_file": "Napa\u010dni podatki iz konfiguracijske datoteke",
|
||||
"link_local_address": "Lokalni naslovi povezave niso podprti"
|
||||
"link_local_address": "Lokalni naslovi povezave niso podprti",
|
||||
"not_axis_device": "Odkrita naprava ni naprava Axis"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Naprava je \u017ee konfigurirana",
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"abort": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
"bad_config_file": "Felaktig data fr\u00e5n config fil",
|
||||
"link_local_address": "Link local addresses are not supported"
|
||||
"link_local_address": "Link local addresses are not supported",
|
||||
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"name": "Braviatv",
|
||||
"documentation": "https://www.home-assistant.io/components/braviatv",
|
||||
"requirements": [
|
||||
"braviarc-homeassistant==0.3.7.dev0"
|
||||
"braviarc-homeassistant==0.3.7.dev0",
|
||||
"getmac==0.8.1"
|
||||
],
|
||||
"dependencies": [
|
||||
"configurator"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
"""Support for interface with a Sony Bravia TV."""
|
||||
import ipaddress
|
||||
import logging
|
||||
import re
|
||||
|
||||
from getmac import get_mac_address
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
|
@ -40,19 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def _get_mac_address(ip_address):
|
||||
"""Get the MAC address of the device."""
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
|
||||
pid_component = pid.communicate()[0]
|
||||
match = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
|
||||
pid_component)
|
||||
if match is not None:
|
||||
return match.groups()[0]
|
||||
return None
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Sony Bravia TV platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
|
@ -84,9 +72,15 @@ def setup_bravia(config, pin, hass, add_entities):
|
|||
request_configuration(config, hass, add_entities)
|
||||
return
|
||||
|
||||
mac = _get_mac_address(host)
|
||||
if mac is not None:
|
||||
mac = mac.decode('utf8')
|
||||
try:
|
||||
if ipaddress.ip_address(host).version == 6:
|
||||
mode = 'ip6'
|
||||
else:
|
||||
mode = 'ip'
|
||||
except ValueError:
|
||||
mode = 'hostname'
|
||||
mac = get_mac_address(**{mode: host})
|
||||
|
||||
# If we came here and configuring this host, mark as done
|
||||
if host in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(host)
|
||||
|
|
|
@ -186,7 +186,7 @@ def _get_camera_from_entity_id(hass, entity_id):
|
|||
component = hass.data.get(DOMAIN)
|
||||
|
||||
if component is None:
|
||||
raise HomeAssistantError('Camera component not set up')
|
||||
raise HomeAssistantError('Camera integration not set up')
|
||||
|
||||
camera = component.get_entity(entity_id)
|
||||
|
||||
|
|
|
@ -286,7 +286,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
|||
"""
|
||||
_LOGGER.warning(
|
||||
'Setting configuration for Cast via platform is deprecated. '
|
||||
'Configure via Cast component instead.')
|
||||
'Configure via Cast integration instead.')
|
||||
await _async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info)
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ class SSLCertificate(Entity):
|
|||
self.server_port = server_port
|
||||
self._name = sensor_name
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -76,34 +77,39 @@ class SSLCertificate(Entity):
|
|||
"""Icon to use in the frontend, if any."""
|
||||
return 'mdi:certificate'
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._available
|
||||
|
||||
def update(self):
|
||||
"""Fetch the certificate information."""
|
||||
ctx = ssl.create_default_context()
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
host_info = socket.getaddrinfo(self.server_name, self.server_port)
|
||||
family = host_info[0][0]
|
||||
sock = ctx.wrap_socket(
|
||||
socket.socket(family=family), server_hostname=self.server_name)
|
||||
sock.settimeout(TIMEOUT)
|
||||
sock.connect((self.server_name, self.server_port))
|
||||
address = (self.server_name, self.server_port)
|
||||
with socket.create_connection(
|
||||
address, timeout=TIMEOUT) as sock:
|
||||
with ctx.wrap_socket(
|
||||
sock, server_hostname=address[0]) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
|
||||
except socket.gaierror:
|
||||
_LOGGER.error("Cannot resolve hostname: %s", self.server_name)
|
||||
self._available = False
|
||||
return
|
||||
except socket.timeout:
|
||||
_LOGGER.error(
|
||||
"Connection timeout with server: %s", self.server_name)
|
||||
self._available = False
|
||||
return
|
||||
except OSError:
|
||||
_LOGGER.error("Cannot connect to %s", self.server_name)
|
||||
return
|
||||
|
||||
try:
|
||||
cert = sock.getpeercert()
|
||||
except OSError:
|
||||
_LOGGER.error("Cannot fetch certificate from %s", self.server_name)
|
||||
_LOGGER.error("Cannot fetch certificate from %s",
|
||||
self.server_name, exc_info=1)
|
||||
self._available = False
|
||||
return
|
||||
|
||||
ts_seconds = ssl.cert_time_to_seconds(cert['notAfter'])
|
||||
timestamp = datetime.fromtimestamp(ts_seconds)
|
||||
expiry = timestamp - datetime.today()
|
||||
self._available = True
|
||||
self._state = expiry.days
|
||||
|
|
|
@ -1,68 +1,41 @@
|
|||
"""Provides functionality to interact with climate devices."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import functools as ft
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_WHOLE,
|
||||
STATE_OFF, STATE_ON, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa
|
||||
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
PRECISION_TENTHS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.helpers.typing import (
|
||||
ConfigType, HomeAssistantType, ServiceDataType)
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import (
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_AWAY_MODE,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_LIST,
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HOLD_MODE,
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_OPERATION_LIST,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_SWING_LIST,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
DOMAIN,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
SERVICE_SET_AWAY_MODE,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HOLD_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||
SUPPORT_TARGET_HUMIDITY_LOW,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_HOLD_MODE,
|
||||
SUPPORT_SWING_MODE,
|
||||
SUPPORT_AWAY_MODE,
|
||||
SUPPORT_AUX_HEAT,
|
||||
)
|
||||
ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HUMIDITY, ATTR_HVAC_ACTIONS,
|
||||
ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES,
|
||||
ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODES,
|
||||
SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE)
|
||||
from .reproduce_state import async_reproduce_states # noqa
|
||||
|
||||
DEFAULT_MIN_TEMP = 7
|
||||
DEFAULT_MAX_TEMP = 35
|
||||
DEFAULT_MIN_HUMITIDY = 30
|
||||
DEFAULT_MIN_HUMIDITY = 30
|
||||
DEFAULT_MAX_HUMIDITY = 99
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
@ -76,14 +49,6 @@ CONVERTIBLE_ATTRIBUTE = [
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ON_OFF_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
})
|
||||
|
||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_AWAY_MODE): cv.boolean,
|
||||
})
|
||||
SET_AUX_HEAT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_AUX_HEAT): cv.boolean,
|
||||
|
@ -96,20 +61,20 @@ SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
|
|||
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
|
||||
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Optional(ATTR_OPERATION_MODE): cv.string,
|
||||
vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES),
|
||||
}
|
||||
))
|
||||
SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_FAN_MODE): cv.string,
|
||||
})
|
||||
SET_HOLD_MODE_SCHEMA = vol.Schema({
|
||||
SET_PRESET_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_HOLD_MODE): cv.string,
|
||||
vol.Required(ATTR_PRESET_MODE): vol.Maybe(cv.string),
|
||||
})
|
||||
SET_OPERATION_MODE_SCHEMA = vol.Schema({
|
||||
SET_HVAC_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(ATTR_OPERATION_MODE): cv.string,
|
||||
vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES),
|
||||
})
|
||||
SET_HUMIDITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
|
@ -121,19 +86,19 @@ SET_SWING_MODE_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up climate devices."""
|
||||
component = hass.data[DOMAIN] = \
|
||||
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA,
|
||||
async_service_away_mode
|
||||
SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA,
|
||||
'async_set_hvac_mode'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_HOLD_MODE, SET_HOLD_MODE_SCHEMA,
|
||||
'async_set_hold_mode'
|
||||
SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA,
|
||||
'async_set_preset_mode'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA,
|
||||
|
@ -151,32 +116,20 @@ async def async_setup(hass, config):
|
|||
SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA,
|
||||
'async_set_fan_mode'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_OPERATION_MODE, SET_OPERATION_MODE_SCHEMA,
|
||||
'async_set_operation_mode'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA,
|
||||
'async_set_swing_mode'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_OFF, ON_OFF_SERVICE_SCHEMA,
|
||||
'async_turn_off'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_ON, ON_OFF_SERVICE_SCHEMA,
|
||||
'async_turn_on'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry):
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
@ -185,27 +138,23 @@ class ClimateDevice(Entity):
|
|||
"""Representation of a climate device."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
"""Return the current state."""
|
||||
if self.is_on is False:
|
||||
return STATE_OFF
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
if self.is_on:
|
||||
return STATE_ON
|
||||
return None
|
||||
return self.hvac_mode
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the system."""
|
||||
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def state_attributes(self) -> Dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
supported_features = self.supported_features
|
||||
data = {
|
||||
ATTR_HVAC_MODES: self.hvac_modes,
|
||||
ATTR_CURRENT_TEMPERATURE: show_temp(
|
||||
self.hass, self.current_temperature, self.temperature_unit,
|
||||
self.precision),
|
||||
|
@ -220,16 +169,13 @@ class ClimateDevice(Entity):
|
|||
self.precision),
|
||||
}
|
||||
|
||||
supported_features = self.supported_features
|
||||
if self.target_temperature_step is not None:
|
||||
if self.target_temperature_step:
|
||||
data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step
|
||||
|
||||
if supported_features & SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||
if supported_features & SUPPORT_TARGET_TEMPERATURE_RANGE:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
self.precision)
|
||||
|
||||
if supported_features & SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
self.precision)
|
||||
|
@ -239,136 +185,160 @@ class ClimateDevice(Entity):
|
|||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY:
|
||||
data[ATTR_HUMIDITY] = self.target_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY_LOW:
|
||||
data[ATTR_MIN_HUMIDITY] = self.min_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY_HIGH:
|
||||
data[ATTR_MAX_HUMIDITY] = self.max_humidity
|
||||
data[ATTR_MIN_HUMIDITY] = self.min_humidity
|
||||
data[ATTR_MAX_HUMIDITY] = self.max_humidity
|
||||
|
||||
if supported_features & SUPPORT_FAN_MODE:
|
||||
data[ATTR_FAN_MODE] = self.current_fan_mode
|
||||
if self.fan_list:
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
data[ATTR_FAN_MODE] = self.fan_mode
|
||||
data[ATTR_FAN_MODES] = self.fan_modes
|
||||
|
||||
if supported_features & SUPPORT_OPERATION_MODE:
|
||||
data[ATTR_OPERATION_MODE] = self.current_operation
|
||||
if self.operation_list:
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
if self.hvac_action:
|
||||
data[ATTR_HVAC_ACTIONS] = self.hvac_action
|
||||
|
||||
if supported_features & SUPPORT_HOLD_MODE:
|
||||
data[ATTR_HOLD_MODE] = self.current_hold_mode
|
||||
if supported_features & SUPPORT_PRESET_MODE:
|
||||
data[ATTR_PRESET_MODE] = self.preset_mode
|
||||
data[ATTR_PRESET_MODES] = self.preset_modes
|
||||
|
||||
if supported_features & SUPPORT_SWING_MODE:
|
||||
data[ATTR_SWING_MODE] = self.current_swing_mode
|
||||
if self.swing_list:
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
|
||||
if supported_features & SUPPORT_AWAY_MODE:
|
||||
is_away = self.is_away_mode_on
|
||||
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
|
||||
data[ATTR_SWING_MODE] = self.swing_mode
|
||||
data[ATTR_SWING_MODES] = self.swing_modes
|
||||
|
||||
if supported_features & SUPPORT_AUX_HEAT:
|
||||
is_aux_heat = self.is_aux_heat_on
|
||||
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
|
||||
data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
def current_humidity(self) -> Optional[int]:
|
||||
"""Return the current humidity."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
def target_humidity(self) -> Optional[int]:
|
||||
"""Return the humidity we try to reach."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return hvac operation ie. heat, cool mode.
|
||||
|
||||
Need to be one of HVAC_MODE_*.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> List[str]:
|
||||
"""Return the list of available hvac operation modes.
|
||||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> Optional[str]:
|
||||
"""Return the current running hvac operation if supported.
|
||||
|
||||
Need to be one of CURRENT_HVAC_*.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> Optional[float]:
|
||||
"""Return the current temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the temperature we try to reach."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
def target_temperature_step(self) -> Optional[float]:
|
||||
"""Return the supported step of target temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
return None
|
||||
def target_temperature_high(self) -> Optional[float]:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
|
||||
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
return None
|
||||
def target_temperature_low(self) -> Optional[float]:
|
||||
"""Return the lowbound target temperature we try to reach.
|
||||
|
||||
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return None
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return the current hold mode, e.g., home, away, temp."""
|
||||
return None
|
||||
def preset_modes(self) -> Optional[List[str]]:
|
||||
"""Return a list of available preset modes.
|
||||
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return None
|
||||
def is_aux_heat(self) -> Optional[str]:
|
||||
"""Return true if aux heater.
|
||||
|
||||
Requires SUPPORT_AUX_HEAT.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
return None
|
||||
def fan_mode(self) -> Optional[str]:
|
||||
"""Return the fan setting.
|
||||
|
||||
Requires SUPPORT_FAN_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return None
|
||||
def fan_modes(self) -> Optional[List[str]]:
|
||||
"""Return the list of available fan modes.
|
||||
|
||||
Requires SUPPORT_FAN_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return None
|
||||
def swing_mode(self) -> Optional[str]:
|
||||
"""Return the swing setting.
|
||||
|
||||
Requires SUPPORT_SWING_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return None
|
||||
def swing_modes(self) -> Optional[List[str]]:
|
||||
"""Return the list of available swing modes.
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""Return the list of available swing modes."""
|
||||
return None
|
||||
Requires SUPPORT_SWING_MODE.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
def set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_temperature(self, **kwargs):
|
||||
def async_set_temperature(self, **kwargs) -> Awaitable[None]:
|
||||
"""Set new target temperature.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
|
@ -376,164 +346,114 @@ class ClimateDevice(Entity):
|
|||
return self.hass.async_add_job(
|
||||
ft.partial(self.set_temperature, **kwargs))
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
def set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_humidity(self, humidity):
|
||||
def async_set_humidity(self, humidity: int) -> Awaitable[None]:
|
||||
"""Set new target humidity.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_humidity, humidity)
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_fan_mode(self, fan_mode):
|
||||
def async_set_fan_mode(self, fan_mode: str) -> Awaitable[None]:
|
||||
"""Set new target fan mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_fan_mode, fan_mode)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode.
|
||||
def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]:
|
||||
"""Set new target hvac mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_operation_mode, operation_mode)
|
||||
return self.hass.async_add_job(self.set_hvac_mode, hvac_mode)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
def set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set new target swing operation."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_swing_mode(self, swing_mode):
|
||||
def async_set_swing_mode(self, swing_mode: str) -> Awaitable[None]:
|
||||
"""Set new target swing operation.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_swing_mode, swing_mode)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on.
|
||||
def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]:
|
||||
"""Set new preset mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_away_mode_on)
|
||||
return self.hass.async_add_job(self.set_preset_mode, preset_mode)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_away_mode_off(self):
|
||||
"""Turn away mode off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_away_mode_off)
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set new target hold mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_hold_mode(self, hold_mode):
|
||||
"""Set new target hold mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_on(self):
|
||||
def async_turn_aux_heat_on(self) -> Awaitable[None]:
|
||||
"""Turn auxiliary heater on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_aux_heat_on)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_off(self):
|
||||
def async_turn_aux_heat_off(self) -> Awaitable[None]:
|
||||
"""Turn auxiliary heater off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_aux_heat_off)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn device on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_on(self):
|
||||
"""Turn device on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_on)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn device off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_off(self):
|
||||
"""Turn device off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_off)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature."""
|
||||
return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS,
|
||||
self.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature."""
|
||||
return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS,
|
||||
self.temperature_unit)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
def min_humidity(self) -> int:
|
||||
"""Return the minimum humidity."""
|
||||
return DEFAULT_MIN_HUMITIDY
|
||||
return DEFAULT_MIN_HUMIDITY
|
||||
|
||||
@property
|
||||
def max_humidity(self):
|
||||
def max_humidity(self) -> int:
|
||||
"""Return the maximum humidity."""
|
||||
return DEFAULT_MAX_HUMIDITY
|
||||
|
||||
|
||||
async def async_service_away_mode(entity, service):
|
||||
"""Handle away mode service."""
|
||||
if service.data[ATTR_AWAY_MODE]:
|
||||
await entity.async_turn_away_mode_on()
|
||||
else:
|
||||
await entity.async_turn_away_mode_off()
|
||||
|
||||
|
||||
async def async_service_aux_heat(entity, service):
|
||||
async def async_service_aux_heat(
|
||||
entity: ClimateDevice, service: ServiceDataType
|
||||
) -> None:
|
||||
"""Handle aux heat service."""
|
||||
if service.data[ATTR_AUX_HEAT]:
|
||||
await entity.async_turn_aux_heat_on()
|
||||
|
@ -541,7 +461,9 @@ async def async_service_aux_heat(entity, service):
|
|||
await entity.async_turn_aux_heat_off()
|
||||
|
||||
|
||||
async def async_service_temperature_set(entity, service):
|
||||
async def async_service_temperature_set(
|
||||
entity: ClimateDevice, service: ServiceDataType
|
||||
) -> None:
|
||||
"""Handle set temperature service."""
|
||||
hass = entity.hass
|
||||
kwargs = {}
|
||||
|
|
|
@ -1,20 +1,104 @@
|
|||
"""Provides the constants needed for component."""
|
||||
|
||||
# All activity disabled / Device is off/standby
|
||||
HVAC_MODE_OFF = 'off'
|
||||
|
||||
# Heating
|
||||
HVAC_MODE_HEAT = 'heat'
|
||||
|
||||
# Cooling
|
||||
HVAC_MODE_COOL = 'cool'
|
||||
|
||||
# The device supports heating/cooling to a range
|
||||
HVAC_MODE_HEAT_COOL = 'heat_cool'
|
||||
|
||||
# The temperature is set based on a schedule, learned behavior, AI or some
|
||||
# other related mechanism. User is not able to adjust the temperature
|
||||
HVAC_MODE_AUTO = 'auto'
|
||||
|
||||
# Device is in Dry/Humidity mode
|
||||
HVAC_MODE_DRY = 'dry'
|
||||
|
||||
# Only the fan is on, not fan and another mode like cool
|
||||
HVAC_MODE_FAN_ONLY = 'fan_only'
|
||||
|
||||
HVAC_MODES = [
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
]
|
||||
|
||||
|
||||
# Device is running an energy-saving mode
|
||||
PRESET_ECO = 'eco'
|
||||
|
||||
# Device is in away mode
|
||||
PRESET_AWAY = 'away'
|
||||
|
||||
# Device turn all valve full up
|
||||
PRESET_BOOST = 'boost'
|
||||
|
||||
# Device is in comfort mode
|
||||
PRESET_COMFORT = 'comfort'
|
||||
|
||||
# Device is in home mode
|
||||
PRESET_HOME = 'home'
|
||||
|
||||
# Device is prepared for sleep
|
||||
PRESET_SLEEP = 'sleep'
|
||||
|
||||
# Device is reacting to activity (e.g. movement sensors)
|
||||
PRESET_ACTIVITY = 'activity'
|
||||
|
||||
|
||||
# Possible fan state
|
||||
FAN_ON = "on"
|
||||
FAN_OFF = "off"
|
||||
FAN_AUTO = "auto"
|
||||
FAN_LOW = "low"
|
||||
FAN_MEDIUM = "medium"
|
||||
FAN_HIGH = "high"
|
||||
FAN_MIDDLE = "middle"
|
||||
FAN_FOCUS = "focus"
|
||||
FAN_DIFFUSE = "diffuse"
|
||||
|
||||
|
||||
# Possible swing state
|
||||
SWING_OFF = "off"
|
||||
SWING_BOTH = "both"
|
||||
SWING_VERTICAL = "vertical"
|
||||
SWING_HORIZONTAL = "horizontal"
|
||||
|
||||
|
||||
# This are support current states of HVAC
|
||||
CURRENT_HVAC_OFF = 'off'
|
||||
CURRENT_HVAC_HEAT = 'heating'
|
||||
CURRENT_HVAC_COOL = 'cooling'
|
||||
CURRENT_HVAC_DRY = 'drying'
|
||||
CURRENT_HVAC_IDLE = 'idle'
|
||||
CURRENT_HVAC_FAN = 'fan'
|
||||
|
||||
|
||||
ATTR_AUX_HEAT = 'aux_heat'
|
||||
ATTR_AWAY_MODE = 'away_mode'
|
||||
ATTR_CURRENT_HUMIDITY = 'current_humidity'
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_FAN_LIST = 'fan_list'
|
||||
ATTR_FAN_MODES = 'fan_modes'
|
||||
ATTR_FAN_MODE = 'fan_mode'
|
||||
ATTR_HOLD_MODE = 'hold_mode'
|
||||
ATTR_PRESET_MODE = 'preset_mode'
|
||||
ATTR_PRESET_MODES = 'preset_modes'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
ATTR_MAX_HUMIDITY = 'max_humidity'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
ATTR_MIN_HUMIDITY = 'min_humidity'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
ATTR_MIN_TEMP = 'min_temp'
|
||||
ATTR_OPERATION_LIST = 'operation_list'
|
||||
ATTR_OPERATION_MODE = 'operation_mode'
|
||||
ATTR_SWING_LIST = 'swing_list'
|
||||
ATTR_HVAC_ACTIONS = 'hvac_action'
|
||||
ATTR_HVAC_MODES = 'hvac_modes'
|
||||
ATTR_HVAC_MODE = 'hvac_mode'
|
||||
ATTR_SWING_MODES = 'swing_modes'
|
||||
ATTR_SWING_MODE = 'swing_mode'
|
||||
ATTR_TARGET_TEMP_HIGH = 'target_temp_high'
|
||||
ATTR_TARGET_TEMP_LOW = 'target_temp_low'
|
||||
|
@ -28,33 +112,17 @@ DEFAULT_MAX_HUMIDITY = 99
|
|||
DOMAIN = 'climate'
|
||||
|
||||
SERVICE_SET_AUX_HEAT = 'set_aux_heat'
|
||||
SERVICE_SET_AWAY_MODE = 'set_away_mode'
|
||||
SERVICE_SET_FAN_MODE = 'set_fan_mode'
|
||||
SERVICE_SET_HOLD_MODE = 'set_hold_mode'
|
||||
SERVICE_SET_PRESET_MODE = 'set_preset_mode'
|
||||
SERVICE_SET_HUMIDITY = 'set_humidity'
|
||||
SERVICE_SET_OPERATION_MODE = 'set_operation_mode'
|
||||
SERVICE_SET_HVAC_MODE = 'set_hvac_mode'
|
||||
SERVICE_SET_SWING_MODE = 'set_swing_mode'
|
||||
SERVICE_SET_TEMPERATURE = 'set_temperature'
|
||||
|
||||
STATE_HEAT = 'heat'
|
||||
STATE_COOL = 'cool'
|
||||
STATE_IDLE = 'idle'
|
||||
STATE_AUTO = 'auto'
|
||||
STATE_MANUAL = 'manual'
|
||||
STATE_DRY = 'dry'
|
||||
STATE_FAN_ONLY = 'fan_only'
|
||||
STATE_ECO = 'eco'
|
||||
|
||||
SUPPORT_TARGET_TEMPERATURE = 1
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH = 2
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW = 4
|
||||
SUPPORT_TARGET_HUMIDITY = 8
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH = 16
|
||||
SUPPORT_TARGET_HUMIDITY_LOW = 32
|
||||
SUPPORT_FAN_MODE = 64
|
||||
SUPPORT_OPERATION_MODE = 128
|
||||
SUPPORT_HOLD_MODE = 256
|
||||
SUPPORT_SWING_MODE = 512
|
||||
SUPPORT_AWAY_MODE = 1024
|
||||
SUPPORT_AUX_HEAT = 2048
|
||||
SUPPORT_ON_OFF = 4096
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE = 2
|
||||
SUPPORT_TARGET_HUMIDITY = 4
|
||||
SUPPORT_FAN_MODE = 8
|
||||
SUPPORT_PRESET_MODE = 16
|
||||
SUPPORT_SWING_MODE = 32
|
||||
SUPPORT_AUX_HEAT = 64
|
||||
|
|
|
@ -2,27 +2,24 @@
|
|||
import asyncio
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.core import Context, State
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_AWAY_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_HOLD_MODE,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_HUMIDITY,
|
||||
SERVICE_SET_AWAY_MODE,
|
||||
HVAC_MODES,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SERVICE_SET_HOLD_MODE,
|
||||
SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
DOMAIN,
|
||||
|
@ -33,9 +30,9 @@ async def _async_reproduce_states(hass: HomeAssistantType,
|
|||
state: State,
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
async def call_service(service: str, keys: Iterable):
|
||||
async def call_service(service: str, keys: Iterable, data=None):
|
||||
"""Call service with set of attributes given."""
|
||||
data = {}
|
||||
data = data or {}
|
||||
data['entity_id'] = state.entity_id
|
||||
for key in keys:
|
||||
if key in state.attributes:
|
||||
|
@ -45,17 +42,13 @@ async def _async_reproduce_states(hass: HomeAssistantType,
|
|||
DOMAIN, service, data,
|
||||
blocking=True, context=context)
|
||||
|
||||
if state.state == STATE_ON:
|
||||
await call_service(SERVICE_TURN_ON, [])
|
||||
elif state.state == STATE_OFF:
|
||||
await call_service(SERVICE_TURN_OFF, [])
|
||||
if state.state in HVAC_MODES:
|
||||
await call_service(
|
||||
SERVICE_SET_HVAC_MODE, [], {ATTR_HVAC_MODE: state.state})
|
||||
|
||||
if ATTR_AUX_HEAT in state.attributes:
|
||||
await call_service(SERVICE_SET_AUX_HEAT, [ATTR_AUX_HEAT])
|
||||
|
||||
if ATTR_AWAY_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_AWAY_MODE, [ATTR_AWAY_MODE])
|
||||
|
||||
if (ATTR_TEMPERATURE in state.attributes) or \
|
||||
(ATTR_TARGET_TEMP_HIGH in state.attributes) or \
|
||||
(ATTR_TARGET_TEMP_LOW in state.attributes):
|
||||
|
@ -64,21 +57,14 @@ async def _async_reproduce_states(hass: HomeAssistantType,
|
|||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW])
|
||||
|
||||
if ATTR_HOLD_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_HOLD_MODE,
|
||||
[ATTR_HOLD_MODE])
|
||||
|
||||
if ATTR_OPERATION_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_OPERATION_MODE,
|
||||
[ATTR_OPERATION_MODE])
|
||||
if ATTR_PRESET_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE])
|
||||
|
||||
if ATTR_SWING_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_SWING_MODE,
|
||||
[ATTR_SWING_MODE])
|
||||
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
|
||||
|
||||
if ATTR_HUMIDITY in state.attributes:
|
||||
await call_service(SERVICE_SET_HUMIDITY,
|
||||
[ATTR_HUMIDITY])
|
||||
await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY])
|
||||
|
||||
|
||||
@bind_hass
|
||||
|
|
|
@ -9,23 +9,14 @@ set_aux_heat:
|
|||
aux_heat:
|
||||
description: New value of axillary heater.
|
||||
example: true
|
||||
set_away_mode:
|
||||
description: Turn away mode on/off for climate device.
|
||||
set_preset_mode:
|
||||
description: Set preset mode for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
away_mode:
|
||||
description: New value of away mode.
|
||||
example: true
|
||||
set_hold_mode:
|
||||
description: Turn hold mode for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
hold_mode:
|
||||
description: New value of hold mode
|
||||
preset_mode:
|
||||
description: New value of preset mode
|
||||
example: 'away'
|
||||
set_temperature:
|
||||
description: Set target temperature of climate device.
|
||||
|
@ -42,9 +33,9 @@ set_temperature:
|
|||
target_temp_low:
|
||||
description: New target low temperature for HVAC.
|
||||
example: 20
|
||||
operation_mode:
|
||||
description: Operation mode to set temperature to. This defaults to current_operation mode if not set, or set incorrectly.
|
||||
example: 'Heat'
|
||||
hvac_mode:
|
||||
description: HVAC operation mode to set temperature to.
|
||||
example: 'heat'
|
||||
set_humidity:
|
||||
description: Set target humidity of climate device.
|
||||
fields:
|
||||
|
@ -63,15 +54,15 @@ set_fan_mode:
|
|||
fan_mode:
|
||||
description: New value of fan mode.
|
||||
example: On Low
|
||||
set_operation_mode:
|
||||
description: Set operation mode for climate device.
|
||||
set_hvac_mode:
|
||||
description: Set HVAC operation mode for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.nest'
|
||||
operation_mode:
|
||||
hvac_mode:
|
||||
description: New value of operation mode.
|
||||
example: Heat
|
||||
example: heat
|
||||
set_swing_mode:
|
||||
description: Set swing operation for climate device.
|
||||
fields:
|
||||
|
@ -81,20 +72,6 @@ set_swing_mode:
|
|||
swing_mode:
|
||||
description: New value of swing mode.
|
||||
|
||||
turn_on:
|
||||
description: Turn climate device on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
turn_off:
|
||||
description: Turn climate device off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time.
|
||||
fields:
|
||||
|
@ -137,13 +114,3 @@ nuheat_resume_program:
|
|||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
sensibo_assume_state:
|
||||
description: Set Sensibo device to external state.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
state:
|
||||
description: State to set.
|
||||
example: 'idle'
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
"""Http views to control the config manager."""
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.generated import config_flows
|
||||
from homeassistant.loader import async_get_config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
|
@ -61,7 +60,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
|
|||
'state': entry.state,
|
||||
'connection_class': entry.connection_class,
|
||||
'supports_options': hasattr(
|
||||
config_entries.HANDLERS[entry.domain],
|
||||
config_entries.HANDLERS.get(entry.domain),
|
||||
'async_get_options_flow'),
|
||||
} for entry in hass.config_entries.async_entries()])
|
||||
|
||||
|
@ -173,7 +172,8 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""List available flow handlers."""
|
||||
return self.json(config_flows.FLOWS)
|
||||
hass = request.app['hass']
|
||||
return self.json(await async_get_config_flows(hass))
|
||||
|
||||
|
||||
class OptionManagerFlowIndexView(FlowManagerIndexView):
|
||||
|
|
|
@ -6,27 +6,26 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY,
|
||||
STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE,
|
||||
HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF)
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE)
|
||||
|
||||
DEFAULT_PORT = 10102
|
||||
|
||||
AVAILABLE_MODES = [STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_DRY,
|
||||
STATE_FAN_ONLY]
|
||||
AVAILABLE_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_FAN_ONLY]
|
||||
|
||||
CM_TO_HA_STATE = {
|
||||
'heat': STATE_HEAT,
|
||||
'cool': STATE_COOL,
|
||||
'auto': STATE_AUTO,
|
||||
'dry': STATE_DRY,
|
||||
'fan': STATE_FAN_ONLY,
|
||||
'heat': HVAC_MODE_HEAT,
|
||||
'cool': HVAC_MODE_COOL,
|
||||
'auto': HVAC_MODE_AUTO,
|
||||
'dry': HVAC_MODE_DRY,
|
||||
'fan': HVAC_MODE_FAN_ONLY,
|
||||
}
|
||||
|
||||
HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()}
|
||||
|
@ -72,7 +71,8 @@ class CoolmasterClimate(ClimateDevice):
|
|||
"""Initialize the climate device."""
|
||||
self._device = device
|
||||
self._uid = device.uid
|
||||
self._operation_list = supported_modes
|
||||
self._hvac_modes = supported_modes
|
||||
self._hvac_mode = None
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = None
|
||||
|
@ -89,7 +89,10 @@ class CoolmasterClimate(ClimateDevice):
|
|||
self._on = status['is_on']
|
||||
|
||||
device_mode = status['mode']
|
||||
self._current_operation = CM_TO_HA_STATE[device_mode]
|
||||
if self._on:
|
||||
self._hvac_mode = CM_TO_HA_STATE[device_mode]
|
||||
else:
|
||||
self._hvac_mode = HVAC_MODE_OFF
|
||||
|
||||
if status['unit'] == 'celsius':
|
||||
self._unit = TEMP_CELSIUS
|
||||
|
@ -127,27 +130,22 @@ class CoolmasterClimate(ClimateDevice):
|
|||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
def hvac_mode(self):
|
||||
"""Return hvac target hvac state."""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_list
|
||||
return self._hvac_modes
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the device is on."""
|
||||
return self._on
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return FAN_MODES
|
||||
|
||||
|
@ -165,18 +163,13 @@ class CoolmasterClimate(ClimateDevice):
|
|||
fan_mode)
|
||||
self._device.set_fan_speed(fan_mode)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new operation mode."""
|
||||
_LOGGER.debug("Setting operation mode of %s to %s", self.unique_id,
|
||||
operation_mode)
|
||||
self._device.set_mode(HA_STATE_TO_CM[operation_mode])
|
||||
hvac_mode)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on."""
|
||||
_LOGGER.debug("Turning %s on", self.unique_id)
|
||||
self._device.turn_on()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off."""
|
||||
_LOGGER.debug("Turning %s off", self.unique_id)
|
||||
self._device.turn_off()
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
self._device.turn_off()
|
||||
else:
|
||||
self._device.set_mode(HA_STATE_TO_CM[hvac_mode])
|
||||
self._device.turn_on()
|
||||
|
|
|
@ -8,10 +8,15 @@ import voluptuous as vol
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MARKER_TYPE = 'marker_type'
|
||||
ATTR_MARKER_LOW_LEVEL = 'marker_low_level'
|
||||
ATTR_MARKER_HIGH_LEVEL = 'marker_high_level'
|
||||
ATTR_PRINTER_NAME = 'printer_name'
|
||||
ATTR_DEVICE_URI = 'device_uri'
|
||||
ATTR_PRINTER_INFO = 'printer_info'
|
||||
ATTR_PRINTER_IS_SHARED = 'printer_is_shared'
|
||||
|
@ -23,11 +28,14 @@ ATTR_PRINTER_TYPE = 'printer_type'
|
|||
ATTR_PRINTER_URI_SUPPORTED = 'printer_uri_supported'
|
||||
|
||||
CONF_PRINTERS = 'printers'
|
||||
CONF_IS_CUPS_SERVER = 'is_cups_server'
|
||||
|
||||
DEFAULT_HOST = '127.0.0.1'
|
||||
DEFAULT_PORT = 631
|
||||
DEFAULT_IS_CUPS_SERVER = True
|
||||
|
||||
ICON = 'mdi:printer'
|
||||
ICON_PRINTER = 'mdi:printer'
|
||||
ICON_MARKER = 'mdi:water'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
|
@ -39,6 +47,8 @@ PRINTER_STATES = {
|
|||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_IS_CUPS_SERVER,
|
||||
default=DEFAULT_IS_CUPS_SERVER): cv.boolean,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
@ -49,21 +59,44 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
printers = config.get(CONF_PRINTERS)
|
||||
is_cups = config.get(CONF_IS_CUPS_SERVER)
|
||||
|
||||
try:
|
||||
data = CupsData(host, port)
|
||||
if is_cups:
|
||||
data = CupsData(host, port, None)
|
||||
data.update()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port)
|
||||
return False
|
||||
if data.available is False:
|
||||
_LOGGER.error("Unable to connect to CUPS server: %s:%s",
|
||||
host, port)
|
||||
raise PlatformNotReady()
|
||||
|
||||
dev = []
|
||||
for printer in printers:
|
||||
if printer not in data.printers:
|
||||
_LOGGER.error("Printer is not present: %s", printer)
|
||||
continue
|
||||
dev.append(CupsSensor(data, printer))
|
||||
|
||||
if "marker-names" in data.attributes[printer]:
|
||||
for marker in data.attributes[printer]["marker-names"]:
|
||||
dev.append(MarkerSensor(data, printer, marker, True))
|
||||
|
||||
add_entities(dev, True)
|
||||
return
|
||||
|
||||
data = CupsData(host, port, printers)
|
||||
data.update()
|
||||
if data.available is False:
|
||||
_LOGGER.error("Unable to connect to IPP printer: %s:%s",
|
||||
host, port)
|
||||
raise PlatformNotReady()
|
||||
|
||||
dev = []
|
||||
for printer in printers:
|
||||
if printer in data.printers:
|
||||
dev.append(CupsSensor(data, printer))
|
||||
else:
|
||||
_LOGGER.error("Printer is not present: %s", printer)
|
||||
continue
|
||||
dev.append(IPPSensor(data, printer))
|
||||
|
||||
if "marker-names" in data.attributes[printer]:
|
||||
for marker in data.attributes[printer]["marker-names"]:
|
||||
dev.append(MarkerSensor(data, printer, marker, False))
|
||||
|
||||
add_entities(dev, True)
|
||||
|
||||
|
@ -76,6 +109,7 @@ class CupsSensor(Entity):
|
|||
self.data = data
|
||||
self._name = printer
|
||||
self._printer = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -85,56 +119,231 @@ class CupsSensor(Entity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._printer is not None:
|
||||
try:
|
||||
return next(v for k, v in PRINTER_STATES.items()
|
||||
if self._printer['printer-state'] == k)
|
||||
except StopIteration:
|
||||
return self._printer['printer-state']
|
||||
if self._printer is None:
|
||||
return None
|
||||
|
||||
key = self._printer['printer-state']
|
||||
return PRINTER_STATES.get(key, key)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
return ICON_PRINTER
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self._printer is not None:
|
||||
return {
|
||||
ATTR_DEVICE_URI: self._printer['device-uri'],
|
||||
ATTR_PRINTER_INFO: self._printer['printer-info'],
|
||||
ATTR_PRINTER_IS_SHARED: self._printer['printer-is-shared'],
|
||||
ATTR_PRINTER_LOCATION: self._printer['printer-location'],
|
||||
ATTR_PRINTER_MODEL: self._printer['printer-make-and-model'],
|
||||
ATTR_PRINTER_STATE_MESSAGE:
|
||||
self._printer['printer-state-message'],
|
||||
ATTR_PRINTER_STATE_REASON:
|
||||
self._printer['printer-state-reasons'],
|
||||
ATTR_PRINTER_TYPE: self._printer['printer-type'],
|
||||
ATTR_PRINTER_URI_SUPPORTED:
|
||||
self._printer['printer-uri-supported'],
|
||||
}
|
||||
if self._printer is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_DEVICE_URI: self._printer['device-uri'],
|
||||
ATTR_PRINTER_INFO: self._printer['printer-info'],
|
||||
ATTR_PRINTER_IS_SHARED: self._printer['printer-is-shared'],
|
||||
ATTR_PRINTER_LOCATION: self._printer['printer-location'],
|
||||
ATTR_PRINTER_MODEL: self._printer['printer-make-and-model'],
|
||||
ATTR_PRINTER_STATE_MESSAGE:
|
||||
self._printer['printer-state-message'],
|
||||
ATTR_PRINTER_STATE_REASON:
|
||||
self._printer['printer-state-reasons'],
|
||||
ATTR_PRINTER_TYPE: self._printer['printer-type'],
|
||||
ATTR_PRINTER_URI_SUPPORTED:
|
||||
self._printer['printer-uri-supported'],
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
self.data.update()
|
||||
self._printer = self.data.printers.get(self._name)
|
||||
self._available = self.data.available
|
||||
|
||||
|
||||
class IPPSensor(Entity):
|
||||
"""Implementation of the IPPSensor.
|
||||
|
||||
This sensor represents the status of the printer.
|
||||
"""
|
||||
|
||||
def __init__(self, data, name):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._attributes = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._attributes['printer-make-and-model']
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return ICON_PRINTER
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._attributes is None:
|
||||
return None
|
||||
|
||||
key = self._attributes['printer-state']
|
||||
return PRINTER_STATES.get(key, key)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self._attributes is None:
|
||||
return None
|
||||
|
||||
state_attributes = {}
|
||||
|
||||
if 'printer-info' in self._attributes:
|
||||
state_attributes[ATTR_PRINTER_INFO] = \
|
||||
self._attributes['printer-info']
|
||||
|
||||
if 'printer-location' in self._attributes:
|
||||
state_attributes[ATTR_PRINTER_LOCATION] = \
|
||||
self._attributes['printer-location']
|
||||
|
||||
if 'printer-state-message' in self._attributes:
|
||||
state_attributes[ATTR_PRINTER_STATE_MESSAGE] = \
|
||||
self._attributes['printer-state-message']
|
||||
|
||||
if 'printer-state-reasons' in self._attributes:
|
||||
state_attributes[ATTR_PRINTER_STATE_REASON] = \
|
||||
self._attributes['printer-state-reasons']
|
||||
|
||||
if 'printer-uri-supported' in self._attributes:
|
||||
state_attributes[ATTR_PRINTER_URI_SUPPORTED] = \
|
||||
self._attributes['printer-uri-supported']
|
||||
|
||||
return state_attributes
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self.data.update()
|
||||
self._attributes = self.data.attributes.get(self._name)
|
||||
self._available = self.data.available
|
||||
|
||||
|
||||
class MarkerSensor(Entity):
|
||||
"""Implementation of the MarkerSensor.
|
||||
|
||||
This sensor represents the percentage of ink or toner.
|
||||
"""
|
||||
|
||||
def __init__(self, data, printer, name, is_cups):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._printer = printer
|
||||
self._index = data.attributes[printer]['marker-names'].index(name)
|
||||
self._is_cups = is_cups
|
||||
self._attributes = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return ICON_MARKER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._attributes is None:
|
||||
return None
|
||||
|
||||
return self._attributes[self._printer]['marker-levels'][self._index]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "%"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self._attributes is None:
|
||||
return None
|
||||
|
||||
high_level = self._attributes[self._printer]['marker-high-levels']
|
||||
if isinstance(high_level, list):
|
||||
high_level = high_level[self._index]
|
||||
|
||||
low_level = self._attributes[self._printer]['marker-low-levels']
|
||||
if isinstance(low_level, list):
|
||||
low_level = low_level[self._index]
|
||||
|
||||
marker_types = self._attributes[self._printer]['marker-types']
|
||||
if isinstance(marker_types, list):
|
||||
marker_types = marker_types[self._index]
|
||||
|
||||
if self._is_cups:
|
||||
printer_name = self._printer
|
||||
else:
|
||||
printer_name = \
|
||||
self._attributes[self._printer]['printer-make-and-model']
|
||||
|
||||
return {
|
||||
ATTR_MARKER_HIGH_LEVEL: high_level,
|
||||
ATTR_MARKER_LOW_LEVEL: low_level,
|
||||
ATTR_MARKER_TYPE: marker_types,
|
||||
ATTR_PRINTER_NAME: printer_name
|
||||
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
# Data fetching is done by CupsSensor/IPPSensor
|
||||
self._attributes = self.data.attributes
|
||||
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
class CupsData:
|
||||
"""Get the latest data from CUPS and update the state."""
|
||||
|
||||
def __init__(self, host, port):
|
||||
def __init__(self, host, port, ipp_printers):
|
||||
"""Initialize the data object."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ipp_printers = ipp_printers
|
||||
self.is_cups = (ipp_printers is None)
|
||||
self.printers = None
|
||||
self.attributes = {}
|
||||
self.available = False
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from CUPS."""
|
||||
cups = importlib.import_module('cups')
|
||||
|
||||
conn = cups.Connection(host=self._host, port=self._port)
|
||||
self.printers = conn.getPrinters()
|
||||
try:
|
||||
conn = cups.Connection(host=self._host, port=self._port)
|
||||
if self.is_cups:
|
||||
self.printers = conn.getPrinters()
|
||||
for printer in self.printers:
|
||||
self.attributes[printer] = conn.getPrinterAttributes(
|
||||
name=printer)
|
||||
else:
|
||||
for ipp_printer in self._ipp_printers:
|
||||
self.attributes[ipp_printer] = conn.getPrinterAttributes(
|
||||
uri="ipp://{}:{}/{}"
|
||||
.format(self._host, self._port, ipp_printer))
|
||||
|
||||
self.available = True
|
||||
except RuntimeError:
|
||||
self.available = False
|
||||
|
|
|
@ -5,14 +5,17 @@ import re
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
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)
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE,
|
||||
SUPPORT_SWING_MODE,
|
||||
HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY,
|
||||
PRESET_AWAY, PRESET_HOME,
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE, ATTR_SWING_MODE,
|
||||
ATTR_PRESET_MODE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import DOMAIN as DAIKIN_DOMAIN
|
||||
|
@ -27,26 +30,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
HA_STATE_TO_DAIKIN = {
|
||||
STATE_FAN_ONLY: 'fan',
|
||||
STATE_DRY: 'dry',
|
||||
STATE_COOL: 'cool',
|
||||
STATE_HEAT: 'hot',
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_OFF: 'off',
|
||||
HVAC_MODE_FAN_ONLY: 'fan',
|
||||
HVAC_MODE_DRY: 'dry',
|
||||
HVAC_MODE_COOL: 'cool',
|
||||
HVAC_MODE_HEAT: 'hot',
|
||||
HVAC_MODE_HEAT_COOL: 'auto',
|
||||
HVAC_MODE_OFF: 'off',
|
||||
}
|
||||
|
||||
DAIKIN_TO_HA_STATE = {
|
||||
'fan': STATE_FAN_ONLY,
|
||||
'dry': STATE_DRY,
|
||||
'cool': STATE_COOL,
|
||||
'hot': STATE_HEAT,
|
||||
'auto': STATE_AUTO,
|
||||
'off': STATE_OFF,
|
||||
'fan': HVAC_MODE_FAN_ONLY,
|
||||
'dry': HVAC_MODE_DRY,
|
||||
'cool': HVAC_MODE_COOL,
|
||||
'hot': HVAC_MODE_HEAT,
|
||||
'auto': HVAC_MODE_HEAT_COOL,
|
||||
'off': HVAC_MODE_OFF,
|
||||
}
|
||||
|
||||
HA_PRESET_TO_DAIKIN = {
|
||||
PRESET_AWAY: 'on',
|
||||
PRESET_HOME: 'off'
|
||||
}
|
||||
|
||||
HA_ATTR_TO_DAIKIN = {
|
||||
ATTR_AWAY_MODE: 'en_hol',
|
||||
ATTR_OPERATION_MODE: 'mode',
|
||||
ATTR_PRESET_MODE: 'en_hol',
|
||||
ATTR_HVAC_MODE: 'mode',
|
||||
ATTR_FAN_MODE: 'f_rate',
|
||||
ATTR_SWING_MODE: 'f_dir',
|
||||
ATTR_INSIDE_TEMPERATURE: 'htemp',
|
||||
|
@ -80,7 +88,7 @@ class DaikinClimate(ClimateDevice):
|
|||
|
||||
self._api = api
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_HVAC_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_FAN_MODE: self._api.device.fan_rate,
|
||||
ATTR_SWING_MODE: list(
|
||||
map(
|
||||
|
@ -90,12 +98,10 @@ class DaikinClimate(ClimateDevice):
|
|||
),
|
||||
}
|
||||
|
||||
self._supported_features = (SUPPORT_ON_OFF
|
||||
| SUPPORT_OPERATION_MODE
|
||||
| SUPPORT_TARGET_TEMPERATURE)
|
||||
self._supported_features = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
if self._api.device.support_away_mode:
|
||||
self._supported_features |= SUPPORT_AWAY_MODE
|
||||
self._supported_features |= SUPPORT_PRESET_MODE
|
||||
|
||||
if self._api.device.support_fan_rate:
|
||||
self._supported_features |= SUPPORT_FAN_MODE
|
||||
|
@ -127,7 +133,7 @@ class DaikinClimate(ClimateDevice):
|
|||
value = self._api.device.represent(daikin_attr)[1].title()
|
||||
elif key == ATTR_SWING_MODE:
|
||||
value = self._api.device.represent(daikin_attr)[1].title()
|
||||
elif key == ATTR_OPERATION_MODE:
|
||||
elif key == ATTR_HVAC_MODE:
|
||||
# Daikin can return also internal states auto-1 or auto-7
|
||||
# and we need to translate them as AUTO
|
||||
daikin_mode = re.sub(
|
||||
|
@ -135,6 +141,10 @@ class DaikinClimate(ClimateDevice):
|
|||
self._api.device.represent(daikin_attr)[1])
|
||||
ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode)
|
||||
value = ha_mode
|
||||
elif key == ATTR_PRESET_MODE:
|
||||
away = (self._api.device.represent(daikin_attr)[1]
|
||||
!= HA_STATE_TO_DAIKIN[HVAC_MODE_OFF])
|
||||
value = PRESET_AWAY if away else PRESET_HOME
|
||||
|
||||
if value is None:
|
||||
_LOGGER.error("Invalid value requested for key %s", key)
|
||||
|
@ -154,15 +164,17 @@ class DaikinClimate(ClimateDevice):
|
|||
values = {}
|
||||
|
||||
for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE,
|
||||
ATTR_OPERATION_MODE]:
|
||||
ATTR_HVAC_MODE, ATTR_PRESET_MODE]:
|
||||
value = settings.get(attr)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
|
||||
if daikin_attr is not None:
|
||||
if attr == ATTR_OPERATION_MODE:
|
||||
if attr == ATTR_HVAC_MODE:
|
||||
values[daikin_attr] = HA_STATE_TO_DAIKIN[value]
|
||||
elif attr == ATTR_PRESET_MODE:
|
||||
values[daikin_attr] = HA_PRESET_TO_DAIKIN[value]
|
||||
elif value in self._list[attr]:
|
||||
values[daikin_attr] = value.lower()
|
||||
else:
|
||||
|
@ -218,21 +230,21 @@ class DaikinClimate(ClimateDevice):
|
|||
await self._set(kwargs)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self.get(ATTR_OPERATION_MODE)
|
||||
return self.get(ATTR_HVAC_MODE)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._list.get(ATTR_OPERATION_MODE)
|
||||
return self._list.get(ATTR_HVAC_MODE)
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set HVAC mode."""
|
||||
await self._set({ATTR_OPERATION_MODE: operation_mode})
|
||||
await self._set({ATTR_HVAC_MODE: hvac_mode})
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_FAN_MODE)
|
||||
|
||||
|
@ -241,12 +253,12 @@ class DaikinClimate(ClimateDevice):
|
|||
await self._set({ATTR_FAN_MODE: fan_mode})
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""List of available fan modes."""
|
||||
return self._list.get(ATTR_FAN_MODE)
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
def swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_SWING_MODE)
|
||||
|
||||
|
@ -255,10 +267,24 @@ class DaikinClimate(ClimateDevice):
|
|||
await self._set({ATTR_SWING_MODE: swing_mode})
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
def swing_modes(self):
|
||||
"""List of available swing modes."""
|
||||
return self._list.get(ATTR_SWING_MODE)
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_PRESET_MODE)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set new target temperature."""
|
||||
await self._set({ATTR_PRESET_MODE: preset_mode})
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""List of available swing modes."""
|
||||
return list(HA_PRESET_TO_DAIKIN)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
await self._api.async_update()
|
||||
|
@ -267,36 +293,3 @@ 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'})
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Bridge ist bereits konfiguriert",
|
||||
"already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.",
|
||||
"no_bridges": "Keine deCON-Bridges entdeckt",
|
||||
"not_deconz_bridge": "Keine deCONZ Bridge entdeckt",
|
||||
"one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz",
|
||||
"updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert"
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ from pydeconz.sensor import Thermostat
|
|||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
|
||||
HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS)
|
||||
from homeassistant.core import callback
|
||||
|
@ -13,6 +13,8 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR
|
|||
from .deconz_device import DeconzDevice
|
||||
from .gateway import get_gateway_from_config_entry
|
||||
|
||||
SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
|
@ -51,32 +53,28 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
class DeconzThermostat(DeconzDevice, ClimateDevice):
|
||||
"""Representation of a deCONZ thermostat."""
|
||||
|
||||
def __init__(self, device, gateway):
|
||||
"""Set up thermostat device."""
|
||||
super().__init__(device, gateway)
|
||||
|
||||
self._features = SUPPORT_ON_OFF
|
||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._features
|
||||
return SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._device.state_on
|
||||
def hvac_mode(self):
|
||||
"""Return hvac operation ie. heat, cool mode.
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn on switch."""
|
||||
data = {'mode': 'auto'}
|
||||
await self._device.async_set_config(data)
|
||||
Need to be one of HVAC_MODE_*.
|
||||
"""
|
||||
if self._device.on:
|
||||
return HVAC_MODE_HEAT
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off switch."""
|
||||
data = {'mode': 'off'}
|
||||
await self._device.async_set_config(data)
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available hvac operation modes.
|
||||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
return SUPPORT_HVAC
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
@ -97,6 +95,15 @@ class DeconzThermostat(DeconzDevice, ClimateDevice):
|
|||
|
||||
await self._device.async_set_config(data)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
data = {'mode': 'auto'}
|
||||
elif hvac_mode == HVAC_MODE_OFF:
|
||||
data = {'mode': 'off'}
|
||||
|
||||
await self._device.async_set_config(data)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
|
|
@ -1,85 +1,138 @@
|
|||
"""Demo platform that offers a fake climate device."""
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_ON_OFF,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE, HVAC_MODE_AUTO)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH
|
||||
SUPPORT_FLAGS = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo climate devices."""
|
||||
add_entities([
|
||||
DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77,
|
||||
None, None, None, None, 'heat', None, None,
|
||||
None, True),
|
||||
DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High',
|
||||
67, 54, 'Off', 'cool', False, None, None, None),
|
||||
DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low',
|
||||
None, None, 'Auto', 'auto', None, 24, 21, None)
|
||||
DemoClimate(
|
||||
name='HeatPump',
|
||||
target_temperature=68,
|
||||
unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
preset=None,
|
||||
current_temperature=77,
|
||||
fan_mode=None,
|
||||
target_humidity=None,
|
||||
current_humidity=None,
|
||||
swing_mode=None,
|
||||
hvac_mode=HVAC_MODE_HEAT,
|
||||
hvac_action=CURRENT_HVAC_HEAT,
|
||||
aux=None,
|
||||
target_temp_high=None,
|
||||
target_temp_low=None,
|
||||
hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
),
|
||||
DemoClimate(
|
||||
name='Hvac',
|
||||
target_temperature=21,
|
||||
unit_of_measurement=TEMP_CELSIUS,
|
||||
preset=None,
|
||||
current_temperature=22,
|
||||
fan_mode='On High',
|
||||
target_humidity=67,
|
||||
current_humidity=54,
|
||||
swing_mode='Off',
|
||||
hvac_mode=HVAC_MODE_COOL,
|
||||
hvac_action=CURRENT_HVAC_COOL,
|
||||
aux=False,
|
||||
target_temp_high=None,
|
||||
target_temp_low=None,
|
||||
hvac_modes=[mode for mode in HVAC_MODES
|
||||
if mode != HVAC_MODE_HEAT_COOL]
|
||||
),
|
||||
DemoClimate(
|
||||
name='Ecobee',
|
||||
target_temperature=None,
|
||||
unit_of_measurement=TEMP_CELSIUS,
|
||||
preset='home',
|
||||
preset_modes=['home', 'eco'],
|
||||
current_temperature=23,
|
||||
fan_mode='Auto Low',
|
||||
target_humidity=None,
|
||||
current_humidity=None,
|
||||
swing_mode='Auto',
|
||||
hvac_mode=HVAC_MODE_HEAT_COOL,
|
||||
hvac_action=None,
|
||||
aux=None,
|
||||
target_temp_high=24,
|
||||
target_temp_low=21,
|
||||
hvac_modes=[HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT])
|
||||
])
|
||||
|
||||
|
||||
class DemoClimate(ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||
away, hold, current_temperature, current_fan_mode,
|
||||
target_humidity, current_humidity, current_swing_mode,
|
||||
current_operation, aux, target_temp_high, target_temp_low,
|
||||
is_on):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
target_temperature,
|
||||
unit_of_measurement,
|
||||
preset,
|
||||
current_temperature,
|
||||
fan_mode,
|
||||
target_humidity,
|
||||
current_humidity,
|
||||
swing_mode,
|
||||
hvac_mode,
|
||||
hvac_action,
|
||||
aux,
|
||||
target_temp_high,
|
||||
target_temp_low,
|
||||
hvac_modes,
|
||||
preset_modes=None,
|
||||
):
|
||||
"""Initialize the climate device."""
|
||||
self._name = name
|
||||
self._support_flags = SUPPORT_FLAGS
|
||||
if target_temperature is not None:
|
||||
self._support_flags = \
|
||||
self._support_flags | SUPPORT_TARGET_TEMPERATURE
|
||||
if away is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
|
||||
if hold is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_HOLD_MODE
|
||||
if current_fan_mode is not None:
|
||||
if preset is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_PRESET_MODE
|
||||
if fan_mode is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
|
||||
if target_humidity is not None:
|
||||
self._support_flags = \
|
||||
self._support_flags | SUPPORT_TARGET_HUMIDITY
|
||||
if current_swing_mode is not None:
|
||||
if swing_mode is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_SWING_MODE
|
||||
if current_operation is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
|
||||
if hvac_action is not None:
|
||||
self._support_flags = self._support_flags
|
||||
if aux is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_AUX_HEAT
|
||||
if target_temp_high is not None:
|
||||
if (HVAC_MODE_HEAT_COOL in hvac_modes or
|
||||
HVAC_MODE_AUTO in hvac_modes):
|
||||
self._support_flags = \
|
||||
self._support_flags | SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
if target_temp_low is not None:
|
||||
self._support_flags = \
|
||||
self._support_flags | SUPPORT_TARGET_TEMPERATURE_LOW
|
||||
if is_on is not None:
|
||||
self._support_flags = self._support_flags | SUPPORT_ON_OFF
|
||||
self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
self._target_temperature = target_temperature
|
||||
self._target_humidity = target_humidity
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._preset = preset
|
||||
self._preset_modes = preset_modes
|
||||
self._current_temperature = current_temperature
|
||||
self._current_humidity = current_humidity
|
||||
self._current_fan_mode = current_fan_mode
|
||||
self._current_operation = current_operation
|
||||
self._current_fan_mode = fan_mode
|
||||
self._hvac_action = hvac_action
|
||||
self._hvac_mode = hvac_mode
|
||||
self._aux = aux
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = ['On Low', 'On High', 'Auto Low', 'Auto High', 'Off']
|
||||
self._operation_list = ['heat', 'cool', 'auto', 'off']
|
||||
self._swing_list = ['Auto', '1', '2', '3', 'Off']
|
||||
self._current_swing_mode = swing_mode
|
||||
self._fan_modes = ['On Low', 'On High', 'Auto Low', 'Auto High', 'Off']
|
||||
self._hvac_modes = hvac_modes
|
||||
self._swing_modes = ['Auto', '1', '2', '3', 'Off']
|
||||
self._target_temperature_high = target_temp_high
|
||||
self._target_temperature_low = target_temp_low
|
||||
self._on = is_on
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -132,46 +185,56 @@ class DemoClimate(ClimateDevice):
|
|||
return self._target_humidity
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_action(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
return self._hvac_action
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_mode(self):
|
||||
"""Return hvac target hvac state."""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_list
|
||||
return self._hvac_modes
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self._away
|
||||
def preset_mode(self):
|
||||
"""Return preset mode."""
|
||||
return self._preset
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return hold mode setting."""
|
||||
return self._hold
|
||||
def preset_modes(self):
|
||||
"""Return preset modes."""
|
||||
return self._preset_modes
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
def is_aux_heat(self):
|
||||
"""Return true if aux heat is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the device is on."""
|
||||
return self._on
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
return self._fan_modes
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@property
|
||||
def swing_mode(self):
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_modes(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_modes
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
@ -179,69 +242,39 @@ class DemoClimate(ClimateDevice):
|
|||
kwargs.get(ATTR_TARGET_TEMP_LOW) is not None:
|
||||
self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
async def async_set_humidity(self, humidity):
|
||||
"""Set new humidity level."""
|
||||
self._target_humidity = humidity
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new swing mode."""
|
||||
self._current_swing_mode = swing_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
self._current_fan_mode = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new operation mode."""
|
||||
self._current_operation = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
self._hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
self._away = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
self._away = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Update hold_mode on."""
|
||||
self._hold = hold_mode
|
||||
self.schedule_update_ha_state()
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Update preset_mode on."""
|
||||
self._preset = preset_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
self._aux = True
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxiliary heater off."""
|
||||
self._aux = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on."""
|
||||
self._on = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off."""
|
||||
self._on = False
|
||||
self.schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -13,6 +13,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
CONF_DESTINATION = 'to'
|
||||
CONF_START = 'from'
|
||||
CONF_OFFSET = 'offset'
|
||||
DEFAULT_OFFSET = timedelta(minutes=0)
|
||||
CONF_ONLY_DIRECT = 'only_direct'
|
||||
DEFAULT_ONLY_DIRECT = False
|
||||
|
||||
|
@ -23,6 +25,7 @@ SCAN_INTERVAL = timedelta(minutes=2)
|
|||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DESTINATION): cv.string,
|
||||
vol.Required(CONF_START): cv.string,
|
||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): cv.time_period,
|
||||
vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean,
|
||||
})
|
||||
|
||||
|
@ -31,18 +34,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
"""Set up the Deutsche Bahn Sensor."""
|
||||
start = config.get(CONF_START)
|
||||
destination = config.get(CONF_DESTINATION)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
only_direct = config.get(CONF_ONLY_DIRECT)
|
||||
|
||||
add_entities([DeutscheBahnSensor(start, destination, only_direct)], True)
|
||||
add_entities([DeutscheBahnSensor(start, destination,
|
||||
offset, only_direct)], True)
|
||||
|
||||
|
||||
class DeutscheBahnSensor(Entity):
|
||||
"""Implementation of a Deutsche Bahn sensor."""
|
||||
|
||||
def __init__(self, start, goal, only_direct):
|
||||
def __init__(self, start, goal, offset, only_direct):
|
||||
"""Initialize the sensor."""
|
||||
self._name = '{} to {}'.format(start, goal)
|
||||
self.data = SchieneData(start, goal, only_direct)
|
||||
self.data = SchieneData(start, goal, offset, only_direct)
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
|
@ -81,12 +86,13 @@ class DeutscheBahnSensor(Entity):
|
|||
class SchieneData:
|
||||
"""Pull data from the bahn.de web page."""
|
||||
|
||||
def __init__(self, start, goal, only_direct):
|
||||
def __init__(self, start, goal, offset, only_direct):
|
||||
"""Initialize the sensor."""
|
||||
import schiene
|
||||
|
||||
self.start = start
|
||||
self.goal = goal
|
||||
self.offset = offset
|
||||
self.only_direct = only_direct
|
||||
self.schiene = schiene.Schiene()
|
||||
self.connections = [{}]
|
||||
|
@ -94,7 +100,8 @@ class SchieneData:
|
|||
def update(self):
|
||||
"""Update the connection data."""
|
||||
self.connections = self.schiene.connections(
|
||||
self.start, self.goal, dt_util.as_local(dt_util.utcnow()),
|
||||
self.start, self.goal,
|
||||
dt_util.as_local(dt_util.utcnow()+self.offset),
|
||||
self.only_direct)
|
||||
|
||||
if not self.connections:
|
||||
|
|
|
@ -37,7 +37,7 @@ async def async_unload_entry(hass, entry):
|
|||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
class DeviceTrackerEntity(Entity):
|
||||
class BaseTrackerEntity(Entity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
@property
|
||||
|
@ -48,6 +48,27 @@ class DeviceTrackerEntity(Entity):
|
|||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {
|
||||
ATTR_SOURCE_TYPE: self.source_type
|
||||
}
|
||||
|
||||
if self.battery_level:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.battery_level
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class TrackerEntity(BaseTrackerEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
@property
|
||||
def location_accuracy(self):
|
||||
"""Return the location accuracy of the device.
|
||||
|
@ -71,11 +92,6 @@ class DeviceTrackerEntity(Entity):
|
|||
"""Return longitude value of the device."""
|
||||
return NotImplementedError
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
|
@ -99,16 +115,27 @@ class DeviceTrackerEntity(Entity):
|
|||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {
|
||||
ATTR_SOURCE_TYPE: self.source_type
|
||||
}
|
||||
|
||||
attr = {}
|
||||
attr.update(super().state_attributes)
|
||||
if self.latitude is not None:
|
||||
attr[ATTR_LATITUDE] = self.latitude
|
||||
attr[ATTR_LONGITUDE] = self.longitude
|
||||
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
|
||||
|
||||
if self.battery_level:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.battery_level
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class ScannerEntity(BaseTrackerEntity):
|
||||
"""Represent a tracked device that is on a scanned network."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.is_connected:
|
||||
return STATE_HOME
|
||||
return STATE_NOT_HOME
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""Return true if the device is connected to the network."""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Discord",
|
||||
"documentation": "https://www.home-assistant.io/components/discord",
|
||||
"requirements": [
|
||||
"discord.py==1.1.1"
|
||||
"discord.py==1.2.2"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Dlna dmr",
|
||||
"documentation": "https://www.home-assistant.io/components/dlna_dmr",
|
||||
"requirements": [
|
||||
"async-upnp-client==0.14.7"
|
||||
"async-upnp-client==0.14.10"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
|
|
@ -79,6 +79,11 @@ def setup(hass, config):
|
|||
"downloading '%s' failed, status_code=%d",
|
||||
url,
|
||||
req.status_code)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
else:
|
||||
if filename is None and \
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
"""Support for Dyson Pure Hot+Cool link fan."""
|
||||
import logging
|
||||
|
||||
from libpurecool.const import HeatMode, HeatState, FocusMode, HeatTarget
|
||||
from libpurecool.dyson_pure_state import DysonPureHotCoolState
|
||||
from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT, SUPPORT_FAN_MODE, FAN_FOCUS,
|
||||
FAN_DIFFUSE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
|
||||
from . import DYSON_DEVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_DIFFUSE = "Diffuse Mode"
|
||||
STATE_FOCUS = "Focus Mode"
|
||||
FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE]
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_COOL]
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
|
||||
| SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_FAN = [FAN_FOCUS, FAN_DIFFUSE]
|
||||
SUPPORT_HVAG = [HVAC_MODE_COOL, HVAC_MODE_HEAT]
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -24,7 +26,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink
|
||||
# Get Dyson Devices from parent component.
|
||||
add_devices(
|
||||
[DysonPureHotCoolLinkDevice(device)
|
||||
|
@ -43,17 +44,17 @@ class DysonPureHotCoolLinkDevice(ClimateDevice):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.async_add_job(self._device.add_message_listener,
|
||||
self.on_message)
|
||||
self.hass.async_add_job(
|
||||
self._device.add_message_listener, self.on_message)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Call when new messages received from the climate."""
|
||||
from libpurecool.dyson_pure_state import DysonPureHotCoolState
|
||||
if not isinstance(message, DysonPureHotCoolState):
|
||||
return
|
||||
|
||||
if isinstance(message, DysonPureHotCoolState):
|
||||
_LOGGER.debug("Message received for climate device %s : %s",
|
||||
self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug(
|
||||
"Message received for climate device %s : %s", self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -101,32 +102,46 @@ class DysonPureHotCoolLinkDevice(ClimateDevice):
|
|||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
from libpurecool.const import HeatMode, HeatState
|
||||
def hvac_mode(self):
|
||||
"""Return hvac operation ie. heat, cool mode.
|
||||
|
||||
Need to be one of HVAC_MODE_*.
|
||||
"""
|
||||
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
|
||||
return HVAC_MODE_HEAT
|
||||
return HVAC_MODE_COOL
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available hvac operation modes.
|
||||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
return SUPPORT_HVAG
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current running hvac operation if supported.
|
||||
|
||||
Need to be one of CURRENT_HVAC_*.
|
||||
"""
|
||||
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
|
||||
if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value:
|
||||
return STATE_HEAT
|
||||
return STATE_IDLE
|
||||
return STATE_COOL
|
||||
return CURRENT_HVAC_HEAT
|
||||
return CURRENT_HVAC_IDLE
|
||||
return CURRENT_HVAC_COOL
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
from libpurecool.const import FocusMode
|
||||
if self._device.state.focus_mode == FocusMode.FOCUS_ON.value:
|
||||
return STATE_FOCUS
|
||||
return STATE_DIFFUSE
|
||||
return FAN_FOCUS
|
||||
return FAN_DIFFUSE
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return FAN_LIST
|
||||
return SUPPORT_FAN
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
@ -138,7 +153,6 @@ class DysonPureHotCoolLinkDevice(ClimateDevice):
|
|||
# Limit the target temperature into acceptable range.
|
||||
target_temp = min(self.max_temp, target_temp)
|
||||
target_temp = max(self.min_temp, target_temp)
|
||||
from libpurecool.const import HeatTarget, HeatMode
|
||||
self._device.set_configuration(
|
||||
heat_target=HeatTarget.celsius(target_temp),
|
||||
heat_mode=HeatMode.HEAT_ON)
|
||||
|
@ -146,19 +160,17 @@ class DysonPureHotCoolLinkDevice(ClimateDevice):
|
|||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
_LOGGER.debug("Set %s focus mode %s", self.name, fan_mode)
|
||||
from libpurecool.const import FocusMode
|
||||
if fan_mode == STATE_FOCUS:
|
||||
if fan_mode == FAN_FOCUS:
|
||||
self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON)
|
||||
elif fan_mode == STATE_DIFFUSE:
|
||||
elif fan_mode == FAN_DIFFUSE:
|
||||
self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
_LOGGER.debug("Set %s heat mode %s", self.name, operation_mode)
|
||||
from libpurecool.const import HeatMode
|
||||
if operation_mode == STATE_HEAT:
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.debug("Set %s heat mode %s", self.name, hvac_mode)
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
self._device.set_configuration(heat_mode=HeatMode.HEAT_ON)
|
||||
elif operation_mode == STATE_COOL:
|
||||
elif hvac_mode == HVAC_MODE_COOL:
|
||||
self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF)
|
||||
|
||||
@property
|
||||
|
|
|
@ -59,6 +59,6 @@ set_speed:
|
|||
entity_id:
|
||||
description: Name(s) of the entities to set the speed for
|
||||
example: 'fan.living_room'
|
||||
timer:
|
||||
dyson_speed:
|
||||
description: Speed
|
||||
example: 1
|
|
@ -59,7 +59,7 @@ def setup(hass, config):
|
|||
conf.get(CONF_HOST), conf.get(CONF_PORT))
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Ebusd component setup started")
|
||||
_LOGGER.debug("Ebusd integration setup started")
|
||||
import ebusdpy
|
||||
ebusdpy.init(server_address)
|
||||
hass.data[DOMAIN] = EbusdData(server_address, circuit)
|
||||
|
@ -74,7 +74,7 @@ def setup(hass, config):
|
|||
hass.services.register(
|
||||
DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write)
|
||||
|
||||
_LOGGER.debug("Ebusd component setup completed")
|
||||
_LOGGER.debug("Ebusd integration setup completed")
|
||||
return True
|
||||
except (socket.timeout, socket.error):
|
||||
return False
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
"""Support for Ecobee Thermostats."""
|
||||
import collections
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE,
|
||||
DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_FAN_MODE,
|
||||
PRESET_AWAY, FAN_AUTO, FAN_ON, CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_COOL
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
@ -23,10 +25,34 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
|||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
PRESET_TEMPERATURE = 'temp'
|
||||
PRESET_VACATION = 'vacation'
|
||||
PRESET_AUX_HEAT_ONLY = 'aux_heat_only'
|
||||
PRESET_HOLD_NEXT_TRANSITION = 'next_transition'
|
||||
PRESET_HOLD_INDEFINITE = 'indefinite'
|
||||
AWAY_MODE = 'awayMode'
|
||||
|
||||
# Order matters, because for reverse mapping we don't want to map HEAT to AUX
|
||||
ECOBEE_HVAC_TO_HASS = collections.OrderedDict([
|
||||
('heat', HVAC_MODE_HEAT),
|
||||
('cool', HVAC_MODE_COOL),
|
||||
('auto', HVAC_MODE_AUTO),
|
||||
('off', HVAC_MODE_OFF),
|
||||
('auxHeatOnly', HVAC_MODE_HEAT),
|
||||
])
|
||||
|
||||
PRESET_TO_ECOBEE_HOLD = {
|
||||
PRESET_HOLD_NEXT_TRANSITION: 'nextTransition',
|
||||
PRESET_HOLD_INDEFINITE: 'indefinite',
|
||||
}
|
||||
|
||||
PRESET_MODES = [
|
||||
PRESET_AWAY,
|
||||
PRESET_TEMPERATURE,
|
||||
PRESET_HOLD_NEXT_TRANSITION,
|
||||
PRESET_HOLD_INDEFINITE
|
||||
]
|
||||
|
||||
SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time'
|
||||
SERVICE_RESUME_PROGRAM = 'ecobee_resume_program'
|
||||
|
||||
|
@ -40,11 +66,9 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
|
|||
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE |
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
@ -114,9 +138,10 @@ class Thermostat(ClimateDevice):
|
|||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self._fan_list = ['auto', 'on']
|
||||
self._operation_list = [
|
||||
HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF
|
||||
]
|
||||
self._fan_modes = [FAN_AUTO, FAN_ON]
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
|
@ -143,6 +168,9 @@ class Thermostat(ClimateDevice):
|
|||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.thermostat['settings']['useCelsius']:
|
||||
return TEMP_CELSIUS
|
||||
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
|
@ -153,25 +181,25 @@ class Thermostat(ClimateDevice):
|
|||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
if self.hvac_mode == HVAC_MODE_AUTO:
|
||||
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
if self.hvac_mode == HVAC_MODE_AUTO:
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
if self.hvac_mode == HVAC_MODE_AUTO:
|
||||
return None
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if self.hvac_mode == HVAC_MODE_HEAT:
|
||||
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
if self.current_operation == STATE_COOL:
|
||||
if self.hvac_mode == HVAC_MODE_COOL:
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
|
@ -180,70 +208,63 @@ class Thermostat(ClimateDevice):
|
|||
"""Return the current fan status."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
mode = self._current_hold_mode
|
||||
return None if mode == AWAY_MODE else mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the available fan modes."""
|
||||
return self._fan_list
|
||||
return self._fan_modes
|
||||
|
||||
@property
|
||||
def _current_hold_mode(self):
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['running']:
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
# A permanent hold from away climate
|
||||
return AWAY_MODE
|
||||
if event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
if event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
if event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return VACATION_HOLD
|
||||
if not event['running']:
|
||||
continue
|
||||
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return PRESET_AWAY
|
||||
# A permanent hold from away climate
|
||||
return PRESET_AWAY
|
||||
if event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return PRESET_TEMPERATURE
|
||||
if event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
if event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return PRESET_VACATION
|
||||
|
||||
if self.is_aux_heat:
|
||||
return PRESET_AUX_HEAT_ONLY
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return current operation."""
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
return self.operation_mode
|
||||
return ECOBEE_HVAC_TO_HASS[self.thermostat['settings']['hvacMode']]
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the operation modes list."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def operation_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self.thermostat['settings']['hvacMode']
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
def climate_mode(self):
|
||||
"""Return current mode, as the user-visible name."""
|
||||
cur = self.thermostat['program']['currentClimateRef']
|
||||
climates = self.thermostat['program']['climates']
|
||||
|
@ -251,80 +272,76 @@ class Thermostat(ClimateDevice):
|
|||
return current[0]['name']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
"""Return current fan minimum on time."""
|
||||
return self.thermostat['settings']['fanMinOnTime']
|
||||
def current_humidity(self) -> Optional[int]:
|
||||
"""Return the current humidity."""
|
||||
return self.thermostat['runtime']['actualHumidity']
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return current HVAC action."""
|
||||
status = self.thermostat['equipmentStatus']
|
||||
operation = None
|
||||
|
||||
if status == '':
|
||||
operation = CURRENT_HVAC_OFF
|
||||
elif 'Cool' in status:
|
||||
operation = CURRENT_HVAC_COOL
|
||||
elif 'auxHeat' in status or 'heatPump' in status:
|
||||
operation = CURRENT_HVAC_HEAT
|
||||
|
||||
return operation
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
# Move these to Thermostat Device and make them global
|
||||
status = self.thermostat['equipmentStatus']
|
||||
operation = None
|
||||
if status == '':
|
||||
operation = STATE_IDLE
|
||||
elif 'Cool' in status:
|
||||
operation = STATE_COOL
|
||||
elif 'auxHeat' in status:
|
||||
operation = STATE_HEAT
|
||||
elif 'heatPump' in status:
|
||||
operation = STATE_HEAT
|
||||
else:
|
||||
operation = status
|
||||
|
||||
return {
|
||||
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"fan": self.fan,
|
||||
"climate_mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_mode": self.climate_mode,
|
||||
"equipment_running": status,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
"fan_min_on_time": self.thermostat['settings']['fanMinOnTime']
|
||||
}
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._current_hold_mode == AWAY_MODE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
def is_aux_heat(self):
|
||||
"""Return true if aux heater."""
|
||||
return 'auxHeat' in self.thermostat['equipmentStatus']
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
if self._current_hold_mode != AWAY_MODE:
|
||||
def set_preset(self, preset):
|
||||
"""Activate a preset."""
|
||||
if preset == self.preset_mode:
|
||||
return
|
||||
|
||||
self.update_without_throttle = True
|
||||
|
||||
# If we are currently in vacation mode, cancel it.
|
||||
if self.preset_mode == PRESET_VACATION:
|
||||
self.data.ecobee.delete_vacation(
|
||||
self.thermostat_index, self.vacation)
|
||||
|
||||
if preset == PRESET_AWAY:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index, 'away',
|
||||
'indefinite')
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
if self._current_hold_mode == AWAY_MODE:
|
||||
elif preset == PRESET_TEMPERATURE:
|
||||
self.set_temp_hold(self.current_temperature)
|
||||
|
||||
elif preset in (PRESET_HOLD_NEXT_TRANSITION, PRESET_HOLD_INDEFINITE):
|
||||
self.data.ecobee.set_climate_hold(
|
||||
self.thermostat_index, PRESET_TO_ECOBEE_HOLD[preset],
|
||||
self.hold_preference())
|
||||
|
||||
elif preset is None:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set hold mode (away, home, temp, sleep, etc.)."""
|
||||
hold = self.current_hold_mode
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
if hold_mode == 'None' or hold_mode is None:
|
||||
if hold == VACATION_HOLD:
|
||||
self.data.ecobee.delete_vacation(
|
||||
self.thermostat_index, self.vacation)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
self.set_temp_hold(self.current_temperature)
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(
|
||||
self.thermostat_index, hold_mode, self.hold_preference())
|
||||
self.update_without_throttle = True
|
||||
_LOGGER.warning("Received invalid preset: %s", preset)
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return available preset modes."""
|
||||
return PRESET_MODES
|
||||
|
||||
def set_auto_temp_hold(self, heat_temp, cool_temp):
|
||||
"""Set temperature hold in auto mode."""
|
||||
|
@ -352,7 +369,8 @@ class Thermostat(ClimateDevice):
|
|||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set the fan mode. Valid values are "on" or "auto"."""
|
||||
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
|
||||
if fan_mode.lower() != STATE_ON and \
|
||||
fan_mode.lower() != HVAC_MODE_AUTO:
|
||||
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
|
@ -376,8 +394,8 @@ class Thermostat(ClimateDevice):
|
|||
heatCoolMinDelta property.
|
||||
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
|
||||
"""
|
||||
if self.current_operation == STATE_HEAT or self.current_operation == \
|
||||
STATE_COOL:
|
||||
if self.hvac_mode == HVAC_MODE_HEAT or \
|
||||
self.hvac_mode == HVAC_MODE_COOL:
|
||||
heat_temp = temp
|
||||
cool_temp = temp
|
||||
else:
|
||||
|
@ -392,7 +410,7 @@ class Thermostat(ClimateDevice):
|
|||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self.current_operation == STATE_AUTO and \
|
||||
if self.hvac_mode == HVAC_MODE_AUTO and \
|
||||
(low_temp is not None or high_temp is not None):
|
||||
self.set_auto_temp_hold(low_temp, high_temp)
|
||||
elif temp is not None:
|
||||
|
@ -405,9 +423,14 @@ class Thermostat(ClimateDevice):
|
|||
"""Set the humidity level."""
|
||||
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)
|
||||
ecobee_value = next((k for k, v in ECOBEE_HVAC_TO_HASS.items()
|
||||
if v == hvac_mode), None)
|
||||
if ecobee_value is None:
|
||||
_LOGGER.error("Invalid mode for set_hvac_mode: %s", hvac_mode)
|
||||
return
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, ecobee_value)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_fan_min_on_time(self, fan_min_on_time):
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
"""Support for control of Elk-M1 connected thermostats."""
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL,
|
||||
STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE)
|
||||
from homeassistant.const import PRECISION_WHOLE, STATE_ON
|
||||
|
||||
from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities
|
||||
|
||||
|
||||
SUPPORT_HVAC = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_AUTO,
|
||||
HVAC_MODE_FAN_ONLY]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Create the Elk-M1 thermostat platform."""
|
||||
|
@ -32,9 +37,8 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT
|
||||
| SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
| SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
return (SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT
|
||||
| SUPPORT_TARGET_TEMPERATURE_RANGE)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
|
@ -78,14 +82,14 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
return self._element.humidity
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY]
|
||||
return SUPPORT_HVAC
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
|
@ -93,7 +97,7 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
def is_aux_heat(self):
|
||||
"""Return if aux heater is on."""
|
||||
from elkm1_lib.const import ThermostatMode
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
|
||||
|
@ -109,11 +113,11 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
return 99
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
from elkm1_lib.const import ThermostatFan
|
||||
if self._element.fan == ThermostatFan.AUTO.value:
|
||||
return STATE_AUTO
|
||||
return HVAC_MODE_AUTO
|
||||
if self._element.fan == ThermostatFan.ON.value:
|
||||
return STATE_ON
|
||||
return None
|
||||
|
@ -125,17 +129,19 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
if fan is not None:
|
||||
self._element.set(ThermostatSetting.FAN.value, fan)
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set thermostat operation mode."""
|
||||
from elkm1_lib.const import ThermostatFan, ThermostatMode
|
||||
settings = {
|
||||
STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
|
||||
STATE_HEAT: (ThermostatMode.HEAT.value, None),
|
||||
STATE_COOL: (ThermostatMode.COOL.value, None),
|
||||
STATE_AUTO: (ThermostatMode.AUTO.value, None),
|
||||
STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value)
|
||||
HVAC_MODE_OFF:
|
||||
(ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
|
||||
HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None),
|
||||
HVAC_MODE_COOL: (ThermostatMode.COOL.value, None),
|
||||
HVAC_MODE_AUTO: (ThermostatMode.AUTO.value, None),
|
||||
HVAC_MODE_FAN_ONLY:
|
||||
(ThermostatMode.OFF.value, ThermostatFan.ON.value)
|
||||
}
|
||||
self._elk_set(settings[operation_mode][0], settings[operation_mode][1])
|
||||
self._elk_set(settings[hvac_mode][0], settings[hvac_mode][1])
|
||||
|
||||
async def async_turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
|
@ -148,14 +154,14 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
self._elk_set(ThermostatMode.HEAT.value, None)
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return [STATE_AUTO, STATE_ON]
|
||||
return [HVAC_MODE_AUTO, STATE_ON]
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
from elkm1_lib.const import ThermostatFan
|
||||
if fan_mode == STATE_AUTO:
|
||||
if fan_mode == HVAC_MODE_AUTO:
|
||||
self._elk_set(None, ThermostatFan.AUTO.value)
|
||||
elif fan_mode == STATE_ON:
|
||||
self._elk_set(None, ThermostatFan.ON.value)
|
||||
|
@ -175,13 +181,13 @@ class ElkThermostat(ElkEntity, ClimateDevice):
|
|||
def _element_changed(self, element, changeset):
|
||||
from elkm1_lib.const import ThermostatFan, ThermostatMode
|
||||
mode_to_state = {
|
||||
ThermostatMode.OFF.value: STATE_IDLE,
|
||||
ThermostatMode.COOL.value: STATE_COOL,
|
||||
ThermostatMode.HEAT.value: STATE_HEAT,
|
||||
ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT,
|
||||
ThermostatMode.AUTO.value: STATE_AUTO,
|
||||
ThermostatMode.OFF.value: HVAC_MODE_OFF,
|
||||
ThermostatMode.COOL.value: HVAC_MODE_COOL,
|
||||
ThermostatMode.HEAT.value: HVAC_MODE_HEAT,
|
||||
ThermostatMode.EMERGENCY_HEAT.value: HVAC_MODE_HEAT,
|
||||
ThermostatMode.AUTO.value: HVAC_MODE_AUTO,
|
||||
}
|
||||
self._state = mode_to_state.get(self._element.mode)
|
||||
if self._state == STATE_IDLE and \
|
||||
if self._state == HVAC_MODE_OFF and \
|
||||
self._element.fan == ThermostatFan.ON.value:
|
||||
self._state = STATE_FAN_ONLY
|
||||
self._state = HVAC_MODE_FAN_ONLY
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Enphase envoy",
|
||||
"documentation": "https://www.home-assistant.io/components/enphase_envoy",
|
||||
"requirements": [
|
||||
"envoy_reader==0.4"
|
||||
"envoy_reader==0.8"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
|
|
@ -7,21 +7,26 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, POWER_WATT)
|
||||
CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, POWER_WATT, ENERGY_WATT_HOUR)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSORS = {
|
||||
"production": ("Envoy Current Energy Production", POWER_WATT),
|
||||
"daily_production": ("Envoy Today's Energy Production", "Wh"),
|
||||
"seven_days_production": ("Envoy Last Seven Days Energy Production", "Wh"),
|
||||
"lifetime_production": ("Envoy Lifetime Energy Production", "Wh"),
|
||||
"consumption": ("Envoy Current Energy Consumption", "W"),
|
||||
"daily_consumption": ("Envoy Today's Energy Consumption", "Wh"),
|
||||
"daily_production": ("Envoy Today's Energy Production", ENERGY_WATT_HOUR),
|
||||
"seven_days_production": ("Envoy Last Seven Days Energy Production",
|
||||
ENERGY_WATT_HOUR),
|
||||
"lifetime_production": ("Envoy Lifetime Energy Production",
|
||||
ENERGY_WATT_HOUR),
|
||||
"consumption": ("Envoy Current Energy Consumption", POWER_WATT),
|
||||
"daily_consumption": ("Envoy Today's Energy Consumption",
|
||||
ENERGY_WATT_HOUR),
|
||||
"seven_days_consumption": ("Envoy Last Seven Days Energy Consumption",
|
||||
"Wh"),
|
||||
"lifetime_consumption": ("Envoy Lifetime Energy Consumption", "Wh")
|
||||
ENERGY_WATT_HOUR),
|
||||
"lifetime_consumption": ("Envoy Lifetime Energy Consumption",
|
||||
ENERGY_WATT_HOUR),
|
||||
"inverters": ("Envoy Inverter", POWER_WATT)
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,15 +39,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.All(cv.ensure_list, [vol.In(list(SENSORS))])})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Enphase Envoy sensor."""
|
||||
from envoy_reader.envoy_reader import EnvoyReader
|
||||
|
||||
ip_address = config[CONF_IP_ADDRESS]
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
|
||||
entities = []
|
||||
# Iterate through the list of sensors
|
||||
for condition in monitored_conditions:
|
||||
add_entities([Envoy(ip_address, condition, SENSORS[condition][0],
|
||||
SENSORS[condition][1])], True)
|
||||
if condition == "inverters":
|
||||
inverters = await EnvoyReader(ip_address).inverters_production()
|
||||
if isinstance(inverters, dict):
|
||||
for inverter in inverters:
|
||||
entities.append(Envoy(ip_address, condition,
|
||||
"{} {}".format(SENSORS[condition][0],
|
||||
inverter),
|
||||
SENSORS[condition][1]))
|
||||
else:
|
||||
entities.append(Envoy(ip_address, condition, SENSORS[condition][0],
|
||||
SENSORS[condition][1]))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class Envoy(Entity):
|
||||
|
@ -76,8 +95,23 @@ class Envoy(Entity):
|
|||
"""Icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the energy production data from the Enphase Envoy."""
|
||||
from envoy_reader import EnvoyReader
|
||||
from envoy_reader.envoy_reader import EnvoyReader
|
||||
|
||||
self._state = getattr(EnvoyReader(self._ip_address), self._type)()
|
||||
if self._type != "inverters":
|
||||
_state = await getattr(EnvoyReader(self._ip_address), self._type)()
|
||||
if isinstance(_state, int):
|
||||
self._state = _state
|
||||
else:
|
||||
_LOGGER.error(_state)
|
||||
self._state = None
|
||||
|
||||
elif self._type == "inverters":
|
||||
inverters = await (EnvoyReader(self._ip_address)
|
||||
.inverters_production())
|
||||
if isinstance(inverters, dict):
|
||||
serial_number = self._name.split(" ")[2]
|
||||
self._state = inverters[serial_number]
|
||||
else:
|
||||
self._state = None
|
||||
|
|
|
@ -5,10 +5,11 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_HEAT, STATE_AUTO, SUPPORT_AUX_HEAT, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, STATE_OFF)
|
||||
ATTR_TEMPERATURE, TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
# Return cached results if last scan was less then this time ago
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
OPERATION_LIST = [STATE_AUTO, STATE_HEAT, STATE_OFF]
|
||||
OPERATION_LIST = [HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
|
@ -24,9 +25,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
EPH_TO_HA_STATE = {
|
||||
'AUTO': STATE_AUTO,
|
||||
'ON': STATE_HEAT,
|
||||
'OFF': STATE_OFF
|
||||
'AUTO': HVAC_MODE_HEAT_COOL,
|
||||
'ON': HVAC_MODE_HEAT,
|
||||
'OFF': HVAC_MODE_OFF
|
||||
}
|
||||
|
||||
HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()}
|
||||
|
@ -65,11 +66,10 @@ class EphEmberThermostat(ClimateDevice):
|
|||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
if self._hot_water:
|
||||
return SUPPORT_AUX_HEAT | SUPPORT_OPERATION_MODE
|
||||
return SUPPORT_AUX_HEAT
|
||||
|
||||
return (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_AUX_HEAT |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_AUX_HEAT)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -100,43 +100,35 @@ class EphEmberThermostat(ClimateDevice):
|
|||
return 1
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
attributes = {
|
||||
'currently_active': self._zone['isCurrentlyActive']
|
||||
}
|
||||
return attributes
|
||||
def hvac_action(self):
|
||||
"""Return current HVAC action."""
|
||||
if self._zone['isCurrentlyActive']:
|
||||
return CURRENT_HVAC_HEAT
|
||||
|
||||
return CURRENT_HVAC_IDLE
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
from pyephember.pyephember import ZoneMode
|
||||
mode = ZoneMode(self._zone['mode'])
|
||||
return self.map_mode_eph_hass(mode)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the supported operations."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set the operation mode."""
|
||||
mode = self.map_mode_hass_eph(operation_mode)
|
||||
mode = self.map_mode_hass_eph(hvac_mode)
|
||||
if mode is not None:
|
||||
self._ember.set_mode_by_name(self._zone_name, mode)
|
||||
else:
|
||||
_LOGGER.error("Invalid operation mode provided %s", operation_mode)
|
||||
_LOGGER.error("Invalid operation mode provided %s", hvac_mode)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return current state."""
|
||||
if self._zone['isCurrentlyActive']:
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
def is_aux_heat(self):
|
||||
"""Return true if aux heater."""
|
||||
return self._zone['isBoostActive']
|
||||
|
||||
|
@ -197,4 +189,4 @@ class EphEmberThermostat(ClimateDevice):
|
|||
@staticmethod
|
||||
def map_mode_eph_hass(operation_mode):
|
||||
"""Map from eph mode to home assistant mode."""
|
||||
return EPH_TO_HA_STATE.get(operation_mode.name, STATE_AUTO)
|
||||
return EPH_TO_HA_STATE.get(operation_mode.name, HVAC_MODE_HEAT_COOL)
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
"""Support for eQ-3 Bluetooth Smart thermostats."""
|
||||
import logging
|
||||
|
||||
import eq3bt as eq3 # pylint: disable=import-error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_HEAT, STATE_MANUAL, STATE_ECO,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST,
|
||||
SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_MAC, CONF_DEVICES, STATE_ON, STATE_OFF,
|
||||
TEMP_CELSIUS, PRECISION_HALVES)
|
||||
ATTR_TEMPERATURE, CONF_DEVICES, CONF_MAC, PRECISION_HALVES, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -23,6 +22,32 @@ ATTR_STATE_LOCKED = 'is_locked'
|
|||
ATTR_STATE_LOW_BAT = 'low_battery'
|
||||
ATTR_STATE_AWAY_END = 'away_end'
|
||||
|
||||
EQ_TO_HA_HVAC = {
|
||||
eq3.Mode.Open: HVAC_MODE_HEAT,
|
||||
eq3.Mode.Closed: HVAC_MODE_OFF,
|
||||
eq3.Mode.Auto: HVAC_MODE_AUTO,
|
||||
eq3.Mode.Manual: HVAC_MODE_HEAT,
|
||||
eq3.Mode.Boost: HVAC_MODE_AUTO,
|
||||
eq3.Mode.Away: HVAC_MODE_HEAT,
|
||||
}
|
||||
|
||||
HA_TO_EQ_HVAC = {
|
||||
HVAC_MODE_HEAT: eq3.Mode.Manual,
|
||||
HVAC_MODE_OFF: eq3.Mode.Closed,
|
||||
HVAC_MODE_AUTO: eq3.Mode.Auto
|
||||
}
|
||||
|
||||
EQ_TO_HA_PRESET = {
|
||||
eq3.Mode.Boost: PRESET_BOOST,
|
||||
eq3.Mode.Away: PRESET_AWAY,
|
||||
}
|
||||
|
||||
HA_TO_EQ_PRESET = {
|
||||
PRESET_BOOST: eq3.Mode.Boost,
|
||||
PRESET_AWAY: eq3.Mode.Away,
|
||||
}
|
||||
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
})
|
||||
|
@ -32,8 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Schema({cv.string: DEVICE_SCHEMA}),
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_ON_OFF)
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
@ -42,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
for name, device_cfg in config[CONF_DEVICES].items():
|
||||
mac = device_cfg[CONF_MAC]
|
||||
devices.append(EQ3BTSmartThermostat(mac, name))
|
||||
devices.append(EQ3BTSmartThermostat(mac, name), True)
|
||||
|
||||
add_entities(devices)
|
||||
|
||||
|
@ -53,23 +77,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
# We want to avoid name clash with this module.
|
||||
import eq3bt as eq3 # pylint: disable=import-error
|
||||
|
||||
self.modes = {
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_HEAT,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_ECO,
|
||||
}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
|
||||
self._name = _name
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
self._target_temperature = None
|
||||
self._target_mode = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -79,7 +88,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if thermostat is available."""
|
||||
return self.current_operation is not None
|
||||
return self._thermostat.mode > 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -111,46 +120,25 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
self._target_temperature = temperature
|
||||
self._thermostat.target_temperature = temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return the current operation mode."""
|
||||
if self._thermostat.mode < 0:
|
||||
return None
|
||||
return self.modes[self._thermostat.mode]
|
||||
return HVAC_MODE_OFF
|
||||
return EQ_TO_HA_HVAC[self._thermostat.mode]
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [x for x in self.modes.values()]
|
||||
return list(HA_TO_EQ_HVAC.keys())
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set operation mode."""
|
||||
self._target_mode = operation_mode
|
||||
self._thermostat.mode = self.reverse_modes[operation_mode]
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Away mode off turns to AUTO mode."""
|
||||
self.set_operation_mode(STATE_HEAT)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Set away mode on."""
|
||||
self.set_operation_mode(STATE_ECO)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if we are away."""
|
||||
return self.current_operation == STATE_ECO
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn device on."""
|
||||
self.set_operation_mode(STATE_HEAT)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn device off."""
|
||||
self.set_operation_mode(STATE_OFF)
|
||||
if self.preset_mode:
|
||||
return
|
||||
self._thermostat.mode = HA_TO_EQ_HVAC[hvac_mode]
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@ -175,6 +163,28 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
|
||||
return dev_specific
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
return EQ_TO_HA_PRESET.get(self._thermostat.mode)
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return a list of available preset modes.
|
||||
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
return list(HA_TO_EQ_PRESET.keys())
|
||||
|
||||
def set_preset_mode(self, preset_mode):
|
||||
"""Set new preset mode."""
|
||||
if not preset_mode:
|
||||
self.set_hvac_mode(HVAC_MODE_HEAT)
|
||||
self._thermostat.mode = HA_TO_EQ_PRESET[preset_mode]
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
|
@ -183,15 +193,3 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
self._thermostat.update()
|
||||
except BTLEException as ex:
|
||||
_LOGGER.warning("Updating the state failed: %s", ex)
|
||||
|
||||
if (self._target_temperature and
|
||||
self._thermostat.target_temperature
|
||||
!= self._target_temperature):
|
||||
self.set_temperature(temperature=self._target_temperature)
|
||||
else:
|
||||
self._target_temperature = None
|
||||
if (self._target_mode and
|
||||
self.modes[self._thermostat.mode] != self._target_mode):
|
||||
self.set_operation_mode(operation_mode=self._target_mode)
|
||||
else:
|
||||
self._target_mode = None
|
||||
|
|
|
@ -6,13 +6,14 @@ from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState
|
|||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY,
|
||||
HVAC_MODE_OFF)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE,
|
||||
STATE_OFF, TEMP_CELSIUS)
|
||||
TEMP_CELSIUS)
|
||||
|
||||
from . import (
|
||||
EsphomeEntity, esphome_map_enum, esphome_state_property,
|
||||
|
@ -34,10 +35,10 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
@esphome_map_enum
|
||||
def _climate_modes():
|
||||
return {
|
||||
ClimateMode.OFF: STATE_OFF,
|
||||
ClimateMode.AUTO: STATE_AUTO,
|
||||
ClimateMode.COOL: STATE_COOL,
|
||||
ClimateMode.HEAT: STATE_HEAT,
|
||||
ClimateMode.OFF: HVAC_MODE_OFF,
|
||||
ClimateMode.AUTO: HVAC_MODE_HEAT_COOL,
|
||||
ClimateMode.COOL: HVAC_MODE_COOL,
|
||||
ClimateMode.HEAT: HVAC_MODE_HEAT,
|
||||
}
|
||||
|
||||
|
||||
|
@ -68,7 +69,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
|
|||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def operation_list(self) -> List[str]:
|
||||
def hvac_modes(self) -> List[str]:
|
||||
"""Return the list of available operation modes."""
|
||||
return [
|
||||
_climate_modes.from_esphome(mode)
|
||||
|
@ -94,18 +95,17 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
|
|||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
features = SUPPORT_OPERATION_MODE
|
||||
features = 0
|
||||
if self._static_info.supports_two_point_target_temperature:
|
||||
features |= (SUPPORT_TARGET_TEMPERATURE_LOW |
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH)
|
||||
features |= (SUPPORT_TARGET_TEMPERATURE_RANGE)
|
||||
else:
|
||||
features |= SUPPORT_TARGET_TEMPERATURE
|
||||
if self._static_info.supports_away:
|
||||
features |= SUPPORT_AWAY_MODE
|
||||
features |= SUPPORT_PRESET_MODE
|
||||
return features
|
||||
|
||||
@esphome_state_property
|
||||
def current_operation(self) -> Optional[str]:
|
||||
def hvac_mode(self) -> Optional[str]:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return _climate_modes.from_esphome(self._state.mode)
|
||||
|
||||
|
@ -129,17 +129,12 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
|
|||
"""Return the highbound target temperature we try to reach."""
|
||||
return self._state.target_temperature_high
|
||||
|
||||
@esphome_state_property
|
||||
def is_away_mode_on(self) -> Optional[bool]:
|
||||
"""Return true if away mode is on."""
|
||||
return self._state.away
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature (and operation mode if set)."""
|
||||
data = {'key': self._static_info.key}
|
||||
if ATTR_OPERATION_MODE in kwargs:
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
data['mode'] = _climate_modes.from_hass(
|
||||
kwargs[ATTR_OPERATION_MODE])
|
||||
kwargs[ATTR_HVAC_MODE])
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
data['target_temperature'] = kwargs[ATTR_TEMPERATURE]
|
||||
if ATTR_TARGET_TEMP_LOW in kwargs:
|
||||
|
@ -155,12 +150,24 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
|
|||
mode=_climate_modes.from_hass(operation_mode),
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self._client.climate_command(key=self._static_info.key,
|
||||
away=True)
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
if self._state and self._state.away:
|
||||
return PRESET_AWAY
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return preset modes."""
|
||||
if self._static_info.supports_away:
|
||||
return [PRESET_AWAY]
|
||||
|
||||
return []
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set preset mode."""
|
||||
away = preset_mode == PRESET_AWAY
|
||||
await self._client.climate_command(key=self._static_info.key,
|
||||
away=False)
|
||||
away=away)
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
"""Support for (EMEA/EU-based) Honeywell evohome systems."""
|
||||
# Glossary:
|
||||
# TCS - temperature control system (a.k.a. Controller, Parent), which can
|
||||
# have up to 13 Children:
|
||||
# 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
|
||||
"""Support for (EMEA/EU-based) Honeywell TCC climate systems.
|
||||
|
||||
Such systems include evohome (multi-zone), and Round Thermostat (single zone).
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from dateutil.tz import tzlocal
|
||||
import requests.exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
import evohomeclient2
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS,
|
||||
PRECISION_HALVES, TEMP_CELSIUS)
|
||||
CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME,
|
||||
HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS, 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_connect, async_dispatcher_send)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time, async_track_time_interval)
|
||||
from homeassistant.util.dt import as_utc, parse_datetime, utcnow
|
||||
|
||||
from .const import (
|
||||
DOMAIN, DATA_EVOHOME, DISPATCHER_EVOHOME, GWS, TCS)
|
||||
from .const import DOMAIN, EVO_STRFTIME, STORAGE_VERSION, STORAGE_KEY, GWS, TCS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ACCESS_TOKEN_EXPIRES = 'access_token_expires'
|
||||
CONF_REFRESH_TOKEN = 'refresh_token'
|
||||
|
||||
CONF_LOCATION_IDX = 'location_idx'
|
||||
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
|
||||
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -44,229 +45,314 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CONF_SECRETS = [
|
||||
CONF_USERNAME, CONF_PASSWORD,
|
||||
]
|
||||
|
||||
# bit masks for dispatcher packets
|
||||
EVO_PARENT = 0x01
|
||||
EVO_CHILD = 0x02
|
||||
def _local_dt_to_utc(dt_naive: datetime) -> datetime:
|
||||
dt_aware = as_utc(dt_naive.replace(microsecond=0, tzinfo=tzlocal()))
|
||||
return dt_aware.replace(tzinfo=None)
|
||||
|
||||
|
||||
def setup(hass, hass_config):
|
||||
"""Create a (EMEA/EU-based) Honeywell evohome system.
|
||||
|
||||
Currently, only the Controller and the Zones are implemented here.
|
||||
"""
|
||||
evo_data = hass.data[DATA_EVOHOME] = {}
|
||||
evo_data['timers'] = {}
|
||||
|
||||
# use a copy, since scan_interval is rounded up to nearest 60s
|
||||
evo_data['params'] = dict(hass_config[DOMAIN])
|
||||
scan_interval = evo_data['params'][CONF_SCAN_INTERVAL]
|
||||
scan_interval = timedelta(
|
||||
minutes=(scan_interval.total_seconds() + 59) // 60)
|
||||
|
||||
def _handle_exception(err):
|
||||
try:
|
||||
client = evo_data['client'] = evohomeclient2.EvohomeClient(
|
||||
evo_data['params'][CONF_USERNAME],
|
||||
evo_data['params'][CONF_PASSWORD],
|
||||
debug=False
|
||||
)
|
||||
raise err
|
||||
|
||||
except evohomeclient2.AuthenticationError as err:
|
||||
except evohomeclient2.AuthenticationError:
|
||||
_LOGGER.error(
|
||||
"setup(): Failed to authenticate with the vendor's server. "
|
||||
"Check your username and password are correct. "
|
||||
"Resolve any errors and restart HA. Message is: %s",
|
||||
"Failed to (re)authenticate with the vendor's server. "
|
||||
"Check that your username and password are correct. "
|
||||
"Message is: %s",
|
||||
err
|
||||
)
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error(
|
||||
"setup(): Unable to connect with the vendor's server. "
|
||||
"Check your network and the vendor's status page. "
|
||||
"Resolve any errors and restart HA."
|
||||
# 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."
|
||||
"Message is: %s",
|
||||
err
|
||||
)
|
||||
return False
|
||||
|
||||
finally: # Redact any config data that's no longer needed
|
||||
for parameter in CONF_SECRETS:
|
||||
evo_data['params'][parameter] = 'REDACTED' \
|
||||
if evo_data['params'][parameter] else None
|
||||
except requests.exceptions.HTTPError:
|
||||
if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
|
||||
_LOGGER.warning(
|
||||
"Vendor says their server is currently unavailable. "
|
||||
"Check the vendor's status page."
|
||||
)
|
||||
return False
|
||||
|
||||
evo_data['status'] = {}
|
||||
if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
|
||||
_LOGGER.warning(
|
||||
"The vendor's API rate limit has been exceeded. "
|
||||
"Consider increasing the %s.", CONF_SCAN_INTERVAL
|
||||
)
|
||||
return False
|
||||
|
||||
# Redact any installation data that's no longer needed
|
||||
for loc in client.installation_info:
|
||||
loc['locationInfo']['locationId'] = 'REDACTED'
|
||||
loc['locationInfo']['locationOwner'] = 'REDACTED'
|
||||
loc['locationInfo']['streetAddress'] = 'REDACTED'
|
||||
loc['locationInfo']['city'] = 'REDACTED'
|
||||
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
|
||||
raise # we don't expect/handle any other HTTPErrors
|
||||
|
||||
# Pull down the installation configuration
|
||||
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
||||
try:
|
||||
evo_data['config'] = client.installation_info[loc_idx]
|
||||
|
||||
except IndexError:
|
||||
_LOGGER.error(
|
||||
"setup(): config error, '%s' = %s, but its valid range is 0-%s. "
|
||||
"Unable to continue. Fix any configuration errors and restart HA.",
|
||||
CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1
|
||||
)
|
||||
async def async_setup(hass, hass_config):
|
||||
"""Create a (EMEA/EU-based) Honeywell evohome system."""
|
||||
broker = EvoBroker(hass, hass_config[DOMAIN])
|
||||
if not await broker.init_client():
|
||||
return False
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
tmp_loc = dict(evo_data['config'])
|
||||
tmp_loc['locationInfo']['postcode'] = 'REDACTED'
|
||||
|
||||
if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW...
|
||||
tmp_loc[GWS][0][TCS][0]['dhw'] = '...'
|
||||
|
||||
_LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc)
|
||||
|
||||
load_platform(hass, 'climate', DOMAIN, {}, hass_config)
|
||||
if broker.tcs.hotwater:
|
||||
_LOGGER.warning("DHW controller detected, however this integration "
|
||||
"does not currently support DHW controllers.")
|
||||
|
||||
if 'dhw' in evo_data['config'][GWS][0][TCS][0]:
|
||||
_LOGGER.warning(
|
||||
"setup(): DHW found, but this component doesn't support DHW."
|
||||
)
|
||||
|
||||
@callback
|
||||
def _first_update(event):
|
||||
"""When HA has started, the hub knows to retrieve it's first update."""
|
||||
pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT}
|
||||
async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt)
|
||||
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
|
||||
async_track_time_interval(
|
||||
hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EvoDevice(Entity):
|
||||
"""Base for any Honeywell evohome device.
|
||||
class EvoBroker:
|
||||
"""Container for evohome client and data."""
|
||||
|
||||
Such devices include the Controller, (up to 12) Heating Zones and
|
||||
def __init__(self, hass, params) -> None:
|
||||
"""Initialize the evohome client and data structure."""
|
||||
self.hass = hass
|
||||
self.params = params
|
||||
|
||||
self.config = self.status = self.timers = {}
|
||||
|
||||
self.client = self.tcs = None
|
||||
self._app_storage = None
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN]['broker'] = self
|
||||
|
||||
async def init_client(self) -> bool:
|
||||
"""Initialse the evohome data broker.
|
||||
|
||||
Return True if this is successful, otherwise return False.
|
||||
"""
|
||||
refresh_token, access_token, access_token_expires = \
|
||||
await self._load_auth_tokens()
|
||||
|
||||
try:
|
||||
client = self.client = await self.hass.async_add_executor_job(
|
||||
evohomeclient2.EvohomeClient,
|
||||
self.params[CONF_USERNAME],
|
||||
self.params[CONF_PASSWORD],
|
||||
False,
|
||||
refresh_token,
|
||||
access_token,
|
||||
access_token_expires
|
||||
)
|
||||
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
if not _handle_exception(err):
|
||||
return False
|
||||
|
||||
else:
|
||||
if access_token != self.client.access_token:
|
||||
await self._save_auth_tokens()
|
||||
|
||||
finally:
|
||||
self.params[CONF_PASSWORD] = 'REDACTED'
|
||||
|
||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||
try:
|
||||
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
||||
|
||||
except IndexError:
|
||||
_LOGGER.error(
|
||||
"Config error: '%s' = %s, but its valid range is 0-%s. "
|
||||
"Unable to continue. "
|
||||
"Fix any configuration errors and restart HA.",
|
||||
CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1
|
||||
)
|
||||
return False
|
||||
|
||||
else:
|
||||
self.tcs = \
|
||||
client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa: E501; pylint: disable=protected-access
|
||||
|
||||
_LOGGER.debug("Config = %s", self.config)
|
||||
|
||||
return True
|
||||
|
||||
async def _load_auth_tokens(self) -> Tuple[str, str, datetime]:
|
||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
app_storage = self._app_storage = await store.async_load()
|
||||
|
||||
if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]:
|
||||
refresh_token = app_storage.get(CONF_REFRESH_TOKEN)
|
||||
access_token = app_storage.get(CONF_ACCESS_TOKEN)
|
||||
at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
|
||||
if at_expires_str:
|
||||
at_expires_dt = as_utc(parse_datetime(at_expires_str))
|
||||
at_expires_dt = at_expires_dt.astimezone(tzlocal())
|
||||
at_expires_dt = at_expires_dt.replace(tzinfo=None)
|
||||
else:
|
||||
at_expires_dt = None
|
||||
|
||||
return (refresh_token, access_token, at_expires_dt)
|
||||
|
||||
return (None, None, None) # account switched: so tokens wont be valid
|
||||
|
||||
async def _save_auth_tokens(self, *args) -> None:
|
||||
access_token_expires_utc = _local_dt_to_utc(
|
||||
self.client.access_token_expires)
|
||||
|
||||
self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
|
||||
self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
|
||||
self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token
|
||||
self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = \
|
||||
access_token_expires_utc.isoformat()
|
||||
|
||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
await store.async_save(self._app_storage)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
self.hass,
|
||||
self._save_auth_tokens,
|
||||
access_token_expires_utc
|
||||
)
|
||||
|
||||
def update(self, *args, **kwargs) -> None:
|
||||
"""Get the latest state data of the entire evohome Location.
|
||||
|
||||
This includes state data for the Controller and all its child devices,
|
||||
such as the operating mode of the Controller and the current temp of
|
||||
its children (e.g. Zones, DHW controller).
|
||||
"""
|
||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||
|
||||
try:
|
||||
status = self.client.locations[loc_idx].status()[GWS][0][TCS][0]
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
_handle_exception(err)
|
||||
else:
|
||||
self.timers['statusUpdated'] = utcnow()
|
||||
|
||||
_LOGGER.debug("Status = %s", status)
|
||||
|
||||
# inform the evohome devices that state data has been updated
|
||||
async_dispatcher_send(self.hass, DOMAIN, {'signal': 'refresh'})
|
||||
|
||||
|
||||
class EvoDevice(Entity):
|
||||
"""Base for any evohome device.
|
||||
|
||||
This includes the Controller, (up to 12) Heating Zones and
|
||||
(optionally) a DHW controller.
|
||||
"""
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize the evohome entity."""
|
||||
self._client = client
|
||||
self._obj = obj_ref
|
||||
self._evo_device = evo_device
|
||||
self._evo_tcs = evo_broker.tcs
|
||||
|
||||
self._name = None
|
||||
self._icon = None
|
||||
self._type = None
|
||||
self._name = self._icon = self._precision = None
|
||||
self._state_attributes = []
|
||||
|
||||
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()
|
||||
self._setpoints = None
|
||||
|
||||
@callback
|
||||
def _connect(self, packet):
|
||||
if packet['to'] & self._type and packet['signal'] == 'refresh':
|
||||
def _refresh(self, packet):
|
||||
if packet['signal'] == 'refresh':
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
def _handle_exception(self, err):
|
||||
try:
|
||||
raise err
|
||||
def get_setpoints(self) -> Dict[str, Any]:
|
||||
"""Return the current/next scheduled switchpoints.
|
||||
|
||||
except evohomeclient2.AuthenticationError:
|
||||
_LOGGER.error(
|
||||
"Failed to (re)authenticate with the vendor's server. "
|
||||
"This may be a temporary error. Message is: %s",
|
||||
err
|
||||
)
|
||||
Only Zones & DHW controllers (but not the TCS) have schedules.
|
||||
"""
|
||||
switchpoints = {}
|
||||
schedule = self._evo_device.schedule()
|
||||
|
||||
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
|
||||
day_time = datetime.now()
|
||||
day_of_week = int(day_time.strftime('%w')) # 0 is Sunday
|
||||
|
||||
# Iterate today's switchpoints until past the current time of day...
|
||||
day = schedule['DailySchedules'][day_of_week]
|
||||
sp_idx = -1 # last switchpoint of the day before
|
||||
for i, tmp in enumerate(day['Switchpoints']):
|
||||
if day_time.strftime('%H:%M:%S') > tmp['TimeOfDay']:
|
||||
sp_idx = i # current setpoint
|
||||
else:
|
||||
raise # we don't expect/handle any other HTTPErrors
|
||||
break
|
||||
|
||||
# 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)
|
||||
# Did the current SP start yesterday? Does the next start SP tomorrow?
|
||||
current_sp_day = -1 if sp_idx == -1 else 0
|
||||
next_sp_day = 1 if sp_idx + 1 == len(day['Switchpoints']) else 0
|
||||
|
||||
for key, offset, idx in [
|
||||
('current', current_sp_day, sp_idx),
|
||||
('next', next_sp_day, (sp_idx + 1) * (1 - next_sp_day))]:
|
||||
|
||||
spt = switchpoints[key] = {}
|
||||
|
||||
sp_date = (day_time + timedelta(days=offset)).strftime('%Y-%m-%d')
|
||||
day = schedule['DailySchedules'][(day_of_week + offset) % 7]
|
||||
switchpoint = day['Switchpoints'][idx]
|
||||
|
||||
dt_naive = datetime.strptime(
|
||||
'{}T{}'.format(sp_date, switchpoint['TimeOfDay']),
|
||||
'%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
spt['target_temp'] = switchpoint['heatSetpoint']
|
||||
spt['from_datetime'] = \
|
||||
_local_dt_to_utc(dt_naive).strftime(EVO_STRFTIME)
|
||||
|
||||
return switchpoints
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Most evohome devices push their state to HA.
|
||||
|
||||
Only the Controller should be polled.
|
||||
"""
|
||||
"""Evohome entities should not be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name to use in the frontend UI."""
|
||||
"""Return the name of the Evohome entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes of the evohome device.
|
||||
def device_state_attributes(self) -> Dict[str, Any]:
|
||||
"""Return the Evohome-specific state attributes."""
|
||||
status = {}
|
||||
for attr in self._state_attributes:
|
||||
if attr != 'setpoints':
|
||||
status[attr] = getattr(self._evo_device, attr)
|
||||
|
||||
This is state data that is not available otherwise, due to the
|
||||
restrictions placed upon ClimateDevice properties, etc. by HA.
|
||||
"""
|
||||
return {'status': self._status}
|
||||
if 'setpoints' in self._state_attributes:
|
||||
status['setpoints'] = self._setpoints
|
||||
|
||||
return {'status': status}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
"""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."""
|
||||
def supported_features(self) -> int:
|
||||
"""Get the flag 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
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
def precision(self) -> float:
|
||||
"""Return the temperature precision to use in the frontend UI."""
|
||||
return self._precision
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""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
|
||||
def update(self) -> None:
|
||||
"""Get the latest state data."""
|
||||
self._setpoints = self.get_setpoints()
|
||||
|
|
|
@ -1,457 +1,331 @@
|
|||
"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems."""
|
||||
from datetime import datetime, timedelta
|
||||
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
import evohomeclient2
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_AUTO, STATE_ECO, STATE_MANUAL, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
CONF_SCAN_INTERVAL, STATE_OFF,)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF,
|
||||
PRESET_AWAY, PRESET_ECO, PRESET_HOME,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE)
|
||||
|
||||
from . import (
|
||||
EvoDevice,
|
||||
CONF_LOCATION_IDX, EVO_CHILD, EVO_PARENT)
|
||||
from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice
|
||||
from .const import (
|
||||
DATA_EVOHOME, DISPATCHER_EVOHOME, GWS, TCS)
|
||||
DOMAIN, EVO_STRFTIME,
|
||||
EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_DAYOFF, EVO_CUSTOM,
|
||||
EVO_HEATOFF, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The Controller's opmode/state and the zone's (inherited) state
|
||||
EVO_RESET = 'AutoWithReset'
|
||||
EVO_AUTO = 'Auto'
|
||||
EVO_AUTOECO = 'AutoWithEco'
|
||||
EVO_AWAY = 'Away'
|
||||
EVO_DAYOFF = 'DayOff'
|
||||
EVO_CUSTOM = 'Custom'
|
||||
EVO_HEATOFF = 'HeatingOff'
|
||||
PRESET_RESET = 'Reset' # reset all child zones to EVO_FOLLOW
|
||||
PRESET_CUSTOM = 'Custom'
|
||||
|
||||
# These are for Zones' opmode, and state
|
||||
EVO_FOLLOW = 'FollowSchedule'
|
||||
EVO_TEMPOVER = 'TemporaryOverride'
|
||||
EVO_PERMOVER = 'PermanentOverride'
|
||||
HA_HVAC_TO_TCS = {
|
||||
HVAC_MODE_OFF: EVO_HEATOFF,
|
||||
HVAC_MODE_HEAT: EVO_AUTO,
|
||||
}
|
||||
HA_PRESET_TO_TCS = {
|
||||
PRESET_AWAY: EVO_AWAY,
|
||||
PRESET_CUSTOM: EVO_CUSTOM,
|
||||
PRESET_ECO: EVO_AUTOECO,
|
||||
PRESET_HOME: EVO_DAYOFF,
|
||||
PRESET_RESET: EVO_RESET,
|
||||
}
|
||||
TCS_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_TCS.items()}
|
||||
|
||||
# For the Controller. NB: evohome treats Away mode as a mode in/of itself,
|
||||
# where HA considers it to 'override' the exising operating mode
|
||||
TCS_STATE_TO_HA = {
|
||||
EVO_RESET: STATE_AUTO,
|
||||
EVO_AUTO: STATE_AUTO,
|
||||
EVO_AUTOECO: STATE_ECO,
|
||||
EVO_AWAY: STATE_AUTO,
|
||||
EVO_DAYOFF: STATE_AUTO,
|
||||
EVO_CUSTOM: STATE_AUTO,
|
||||
EVO_HEATOFF: STATE_OFF
|
||||
HA_PRESET_TO_EVO = {
|
||||
'temporary': EVO_TEMPOVER,
|
||||
'permanent': EVO_PERMOVER,
|
||||
}
|
||||
HA_STATE_TO_TCS = {
|
||||
STATE_AUTO: EVO_AUTO,
|
||||
STATE_ECO: EVO_AUTOECO,
|
||||
STATE_OFF: EVO_HEATOFF
|
||||
}
|
||||
TCS_OP_LIST = list(HA_STATE_TO_TCS)
|
||||
|
||||
# the Zones' opmode; their state is usually 'inherited' from the TCS
|
||||
EVO_FOLLOW = 'FollowSchedule'
|
||||
EVO_TEMPOVER = 'TemporaryOverride'
|
||||
EVO_PERMOVER = 'PermanentOverride'
|
||||
|
||||
# for the Zones...
|
||||
ZONE_STATE_TO_HA = {
|
||||
EVO_FOLLOW: STATE_AUTO,
|
||||
EVO_TEMPOVER: STATE_MANUAL,
|
||||
EVO_PERMOVER: STATE_MANUAL
|
||||
}
|
||||
HA_STATE_TO_ZONE = {
|
||||
STATE_AUTO: EVO_FOLLOW,
|
||||
STATE_MANUAL: EVO_PERMOVER
|
||||
}
|
||||
ZONE_OP_LIST = list(HA_STATE_TO_ZONE)
|
||||
EVO_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_EVO.items()}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, hass_config, async_add_entities,
|
||||
discovery_info=None):
|
||||
discovery_info=None) -> None:
|
||||
"""Create the evohome Controller, and its Zones, if any."""
|
||||
evo_data = hass.data[DATA_EVOHOME]
|
||||
|
||||
client = evo_data['client']
|
||||
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
||||
|
||||
# evohomeclient has exposed no means of accessing non-default location
|
||||
# (i.e. loc_idx > 0) other than using a protected member, such as below
|
||||
tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa: E501; pylint: disable=protected-access
|
||||
broker = hass.data[DOMAIN]['broker']
|
||||
loc_idx = broker.params[CONF_LOCATION_IDX]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Found Controller, id=%s [%s], name=%s (location_idx=%s)",
|
||||
tcs_obj_ref.systemId, tcs_obj_ref.modelType, tcs_obj_ref.location.name,
|
||||
broker.tcs.systemId, broker.tcs.modelType, broker.tcs.location.name,
|
||||
loc_idx)
|
||||
|
||||
controller = EvoController(evo_data, client, tcs_obj_ref)
|
||||
zones = []
|
||||
controller = EvoController(broker, broker.tcs)
|
||||
|
||||
for zone_idx in tcs_obj_ref.zones:
|
||||
zone_obj_ref = tcs_obj_ref.zones[zone_idx]
|
||||
zones = []
|
||||
for zone_idx in broker.tcs.zones:
|
||||
evo_zone = broker.tcs.zones[zone_idx]
|
||||
_LOGGER.debug(
|
||||
"Found Zone, id=%s [%s], name=%s",
|
||||
zone_obj_ref.zoneId, zone_obj_ref.zone_type, zone_obj_ref.name)
|
||||
zones.append(EvoZone(evo_data, client, zone_obj_ref))
|
||||
evo_zone.zoneId, evo_zone.zone_type, evo_zone.name)
|
||||
zones.append(EvoZone(broker, evo_zone))
|
||||
|
||||
entities = [controller] + zones
|
||||
|
||||
async_add_entities(entities, update_before_add=False)
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class EvoZone(EvoDevice, ClimateDevice):
|
||||
"""Base for a Honeywell evohome Zone device."""
|
||||
class EvoClimateDevice(EvoDevice, ClimateDevice):
|
||||
"""Base for a Honeywell evohome Climate device."""
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize the evohome Climate device."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._hvac_modes = self._preset_modes = None
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> List[str]:
|
||||
"""Return the list of available hvac operation modes."""
|
||||
return self._hvac_modes
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> Optional[List[str]]:
|
||||
"""Return a list of available preset modes."""
|
||||
return self._preset_modes
|
||||
|
||||
|
||||
class EvoZone(EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome Zone."""
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize the evohome Zone."""
|
||||
super().__init__(evo_data, client, obj_ref)
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._id = obj_ref.zoneId
|
||||
self._name = obj_ref.name
|
||||
self._icon = "mdi:radiator"
|
||||
self._type = EVO_CHILD
|
||||
self._id = evo_device.zoneId
|
||||
self._name = evo_device.name
|
||||
self._icon = 'mdi:radiator'
|
||||
|
||||
for _zone in evo_data['config'][GWS][0][TCS][0]['zones']:
|
||||
self._precision = \
|
||||
self._evo_device.setpointCapabilities['valueResolution']
|
||||
self._state_attributes = [
|
||||
'activeFaults', 'setpointStatus', 'temperatureStatus', 'setpoints']
|
||||
|
||||
self._supported_features = SUPPORT_PRESET_MODE | \
|
||||
SUPPORT_TARGET_TEMPERATURE
|
||||
self._hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
|
||||
self._preset_modes = list(HA_PRESET_TO_EVO)
|
||||
|
||||
for _zone in evo_broker.config['zones']:
|
||||
if _zone['zoneId'] == self._id:
|
||||
self._config = _zone
|
||||
break
|
||||
self._status = {}
|
||||
|
||||
self._operation_list = ZONE_OP_LIST
|
||||
self._supported_features = \
|
||||
SUPPORT_OPERATION_MODE | \
|
||||
SUPPORT_TARGET_TEMPERATURE | \
|
||||
SUPPORT_ON_OFF
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return the current operating mode of the evohome Zone.
|
||||
|
||||
The evohome Zones that are in 'FollowSchedule' mode inherit their
|
||||
actual operating mode from the Controller.
|
||||
"""
|
||||
evo_data = self.hass.data[DATA_EVOHOME]
|
||||
NB: evohome Zones 'inherit' their operating mode from the controller.
|
||||
|
||||
system_mode = evo_data['status']['systemModeStatus']['mode']
|
||||
setpoint_mode = self._status['setpointStatus']['setpointMode']
|
||||
|
||||
if setpoint_mode == EVO_FOLLOW:
|
||||
# then inherit state from the controller
|
||||
if system_mode == EVO_RESET:
|
||||
current_operation = TCS_STATE_TO_HA.get(EVO_AUTO)
|
||||
else:
|
||||
current_operation = TCS_STATE_TO_HA.get(system_mode)
|
||||
else:
|
||||
current_operation = ZONE_STATE_TO_HA.get(setpoint_mode)
|
||||
|
||||
return current_operation
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature of the evohome Zone."""
|
||||
return (self._status['temperatureStatus']['temperature']
|
||||
if self._status['temperatureStatus']['isAvailable'] else None)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature of the evohome Zone."""
|
||||
return self._status['setpointStatus']['targetHeatTemperature']
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the evohome Zone is off.
|
||||
|
||||
A Zone is considered off if its target temp is set to its minimum, and
|
||||
it is not following its schedule (i.e. not in 'FollowSchedule' mode).
|
||||
"""
|
||||
is_off = \
|
||||
self.target_temperature == self.min_temp and \
|
||||
self._status['setpointStatus']['setpointMode'] == EVO_PERMOVER
|
||||
return not is_off
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum target temperature of a evohome Zone.
|
||||
|
||||
The default is 5 (in Celsius), but it is configurable within 5-35.
|
||||
"""
|
||||
return self._config['setpointCapabilities']['minHeatSetpoint']
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum target temperature of a evohome Zone.
|
||||
|
||||
The default is 35 (in Celsius), but it is configurable within 5-35.
|
||||
"""
|
||||
return self._config['setpointCapabilities']['maxHeatSetpoint']
|
||||
|
||||
def _set_temperature(self, temperature, until=None):
|
||||
"""Set the new target temperature of a Zone.
|
||||
|
||||
temperature is required, until can be:
|
||||
- strftime('%Y-%m-%dT%H:%M:%SZ') for TemporaryOverride, or
|
||||
- None for PermanentOverride (i.e. indefinitely)
|
||||
"""
|
||||
try:
|
||||
self._obj.set_temperature(temperature, until)
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
self._handle_exception(err)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature, indefinitely."""
|
||||
self._set_temperature(kwargs['temperature'], until=None)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the evohome Zone on.
|
||||
|
||||
This is achieved by setting the Zone to its 'FollowSchedule' mode.
|
||||
"""
|
||||
self._set_operation_mode(EVO_FOLLOW)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the evohome Zone off.
|
||||
|
||||
This is achieved by setting the Zone to its minimum temperature,
|
||||
indefinitely (i.e. 'PermanentOverride' mode).
|
||||
"""
|
||||
self._set_temperature(self.min_temp, until=None)
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
if operation_mode == EVO_FOLLOW:
|
||||
try:
|
||||
self._obj.cancel_temp_override()
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
self._handle_exception(err)
|
||||
|
||||
elif operation_mode == EVO_TEMPOVER:
|
||||
_LOGGER.error(
|
||||
"_set_operation_mode(op_mode=%s): mode not yet implemented",
|
||||
operation_mode
|
||||
)
|
||||
|
||||
elif operation_mode == EVO_PERMOVER:
|
||||
self._set_temperature(self.target_temperature, until=None)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"_set_operation_mode(op_mode=%s): mode not valid",
|
||||
operation_mode
|
||||
)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set an operating mode for a Zone.
|
||||
|
||||
Currently limited to 'Auto' & 'Manual'. If 'Off' is needed, it can be
|
||||
enabled via turn_off method.
|
||||
|
||||
NB: evohome Zones do not have an operating mode as understood by HA.
|
||||
Instead they usually 'inherit' an operating mode from their controller.
|
||||
|
||||
More correctly, these Zones are in a follow mode, 'FollowSchedule',
|
||||
where their setpoint temperatures are a function of their schedule, and
|
||||
the Controller's operating_mode, e.g. Economy mode is their scheduled
|
||||
setpoint less (usually) 3C.
|
||||
|
||||
Thus, you cannot set a Zone to Away mode, but the location (i.e. the
|
||||
Controller) is set to Away and each Zones's setpoints are adjusted
|
||||
accordingly to some lower temperature.
|
||||
Usually, Zones are in 'FollowSchedule' mode, where their setpoints are
|
||||
a function of their schedule, and the Controller's operating_mode, e.g.
|
||||
Economy mode is their scheduled setpoint less (usually) 3C.
|
||||
|
||||
However, Zones can override these setpoints, either for a specified
|
||||
period of time, 'TemporaryOverride', after which they will revert back
|
||||
to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'.
|
||||
"""
|
||||
self._set_operation_mode(HA_STATE_TO_ZONE.get(operation_mode))
|
||||
if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]:
|
||||
return HVAC_MODE_AUTO
|
||||
is_off = self.target_temperature <= self.min_temp
|
||||
return HVAC_MODE_OFF if is_off else HVAC_MODE_HEAT
|
||||
|
||||
def update(self):
|
||||
"""Process the evohome Zone's state data."""
|
||||
evo_data = self.hass.data[DATA_EVOHOME]
|
||||
@property
|
||||
def current_temperature(self) -> Optional[float]:
|
||||
"""Return the current temperature of the evohome Zone."""
|
||||
return (self._evo_device.temperatureStatus['temperature']
|
||||
if self._evo_device.temperatureStatus['isAvailable'] else None)
|
||||
|
||||
for _zone in evo_data['status']['zones']:
|
||||
if _zone['zoneId'] == self._id:
|
||||
self._status = _zone
|
||||
break
|
||||
@property
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the target temperature of the evohome Zone."""
|
||||
if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF:
|
||||
return self._evo_device.setpointCapabilities['minHeatSetpoint']
|
||||
return self._evo_device.setpointStatus['targetHeatTemperature']
|
||||
|
||||
self._available = True
|
||||
@property
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]:
|
||||
return None
|
||||
return EVO_PRESET_TO_HA.get(
|
||||
self._evo_device.setpointStatus['setpointMode'], 'follow')
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum target temperature of a evohome Zone.
|
||||
|
||||
The default is 5, but is user-configurable within 5-35 (in Celsius).
|
||||
"""
|
||||
return self._evo_device.setpointCapabilities['minHeatSetpoint']
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum target temperature of a evohome Zone.
|
||||
|
||||
The default is 35, but is user-configurable within 5-35 (in Celsius).
|
||||
"""
|
||||
return self._evo_device.setpointCapabilities['maxHeatSetpoint']
|
||||
|
||||
def _set_temperature(self, temperature: float,
|
||||
until: Optional[datetime] = None):
|
||||
"""Set a new target temperature for the Zone.
|
||||
|
||||
until == None means indefinitely (i.e. PermanentOverride)
|
||||
"""
|
||||
try:
|
||||
self._evo_device.set_temperature(temperature, until)
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
_handle_exception(err)
|
||||
|
||||
def set_temperature(self, **kwargs) -> None:
|
||||
"""Set a new target temperature for an hour."""
|
||||
until = kwargs.get('until')
|
||||
if until:
|
||||
until = datetime.strptime(until, EVO_STRFTIME)
|
||||
|
||||
self._set_temperature(kwargs['temperature'], until)
|
||||
|
||||
def _set_operation_mode(self, op_mode) -> None:
|
||||
"""Set the Zone to one of its native EVO_* operating modes."""
|
||||
if op_mode == EVO_FOLLOW:
|
||||
try:
|
||||
self._evo_device.cancel_temp_override()
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
_handle_exception(err)
|
||||
return
|
||||
|
||||
self._setpoints = self.get_setpoints()
|
||||
temperature = self._evo_device.setpointStatus['targetHeatTemperature']
|
||||
|
||||
if op_mode == EVO_TEMPOVER:
|
||||
until = self._setpoints['next']['from_datetime']
|
||||
until = datetime.strptime(until, EVO_STRFTIME)
|
||||
else: # EVO_PERMOVER:
|
||||
until = None
|
||||
|
||||
self._set_temperature(temperature, until=until)
|
||||
|
||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set an operating mode for the Zone."""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
self._set_temperature(self.min_temp, until=None)
|
||||
|
||||
else: # HVAC_MODE_HEAT
|
||||
self._set_operation_mode(EVO_FOLLOW)
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set a new preset mode.
|
||||
|
||||
If preset_mode is None, then revert to following the schedule.
|
||||
"""
|
||||
self._set_operation_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
||||
|
||||
|
||||
class EvoController(EvoDevice, ClimateDevice):
|
||||
"""Base for a Honeywell evohome hub/Controller device.
|
||||
class EvoController(EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome Controller (hub).
|
||||
|
||||
The Controller (aka TCS, temperature control system) is the parent of all
|
||||
the child (CH/DHW) devices. It is also a Climate device.
|
||||
"""
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize the evohome Controller (hub)."""
|
||||
super().__init__(evo_data, client, obj_ref)
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._id = obj_ref.systemId
|
||||
self._name = '_{}'.format(obj_ref.location.name)
|
||||
self._icon = "mdi:thermostat"
|
||||
self._type = EVO_PARENT
|
||||
self._id = evo_device.systemId
|
||||
self._name = evo_device.location.name
|
||||
self._icon = 'mdi:thermostat'
|
||||
|
||||
self._config = evo_data['config'][GWS][0][TCS][0]
|
||||
self._status = evo_data['status']
|
||||
self._timers['statusUpdated'] = datetime.min
|
||||
self._precision = None
|
||||
self._state_attributes = [
|
||||
'activeFaults', 'systemModeStatus']
|
||||
|
||||
self._operation_list = TCS_OP_LIST
|
||||
self._supported_features = \
|
||||
SUPPORT_OPERATION_MODE | \
|
||||
SUPPORT_AWAY_MODE
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._hvac_modes = list(HA_HVAC_TO_TCS)
|
||||
self._preset_modes = list(HA_PRESET_TO_TCS)
|
||||
|
||||
self._config = dict(evo_broker.config)
|
||||
self._config['zones'] = '...'
|
||||
if 'dhw' in self._config:
|
||||
self._config['dhw'] = '...'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes of the evohome Controller.
|
||||
|
||||
This is state data that is not available otherwise, due to the
|
||||
restrictions placed upon ClimateDevice properties, etc. by HA.
|
||||
"""
|
||||
status = dict(self._status)
|
||||
|
||||
if 'zones' in status:
|
||||
del status['zones']
|
||||
if 'dhw' in status:
|
||||
del status['dhw']
|
||||
|
||||
return {'status': status}
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return the current operating mode of the evohome Controller."""
|
||||
return TCS_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
|
||||
tcs_mode = self._evo_device.systemModeStatus['mode']
|
||||
return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the average current temperature of the Heating/DHW zones.
|
||||
def current_temperature(self) -> Optional[float]:
|
||||
"""Return the average current temperature of the heating Zones.
|
||||
|
||||
Although evohome Controllers do not have a target temp, one is
|
||||
expected by the HA schema.
|
||||
Controllers do not have a current temp, but one is expected by HA.
|
||||
"""
|
||||
tmp_list = [x for x in self._status['zones']
|
||||
if x['temperatureStatus']['isAvailable']]
|
||||
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
temps = [z.temperatureStatus['temperature'] for z in
|
||||
self._evo_device._zones if z.temperatureStatus['isAvailable']] # noqa: E501; pylint: disable=protected-access
|
||||
return round(sum(temps) / len(temps), 1) if temps else None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the average target temperature of the Heating/DHW zones.
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the average target temperature of the heating Zones.
|
||||
|
||||
Although evohome Controllers do not have a target temp, one is
|
||||
expected by the HA schema.
|
||||
Controllers do not have a target temp, but one is expected by HA.
|
||||
"""
|
||||
temps = [zone['setpointStatus']['targetHeatTemperature']
|
||||
for zone in self._status['zones']]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
temps = [z.setpointStatus['targetHeatTemperature']
|
||||
for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access
|
||||
return round(sum(temps) / len(temps), 1) if temps else None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return True if away mode is on."""
|
||||
return self._status['systemModeStatus']['mode'] == EVO_AWAY
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
return TCS_PRESET_TO_HA.get(self._evo_device.systemModeStatus['mode'])
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True as evohome Controllers are always on.
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum target temperature of the heating Zones.
|
||||
|
||||
For example, evohome Controllers have a 'HeatingOff' mode, but even
|
||||
then the DHW would remain on.
|
||||
Controllers do not have a min target temp, but one is required by HA.
|
||||
"""
|
||||
return True
|
||||
temps = [z.setpointCapabilities['minHeatSetpoint']
|
||||
for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access
|
||||
return min(temps) if temps else 5
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum target temperature of a evohome Controller.
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum target temperature of the heating Zones.
|
||||
|
||||
Although evohome Controllers do not have a minimum target temp, one is
|
||||
expected by the HA schema; the default for an evohome HR92 is used.
|
||||
Controllers do not have a max target temp, but one is required by HA.
|
||||
"""
|
||||
return 5
|
||||
temps = [z.setpointCapabilities['maxHeatSetpoint']
|
||||
for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access
|
||||
return max(temps) if temps else 35
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum target temperature of a evohome Controller.
|
||||
|
||||
Although evohome Controllers do not have a maximum target temp, one is
|
||||
expected by the HA schema; the default for an evohome HR92 is used.
|
||||
"""
|
||||
return 35
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True as the evohome Controller should always be polled."""
|
||||
return True
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
def _set_operation_mode(self, op_mode) -> None:
|
||||
"""Set the Controller to any of its native EVO_* operating modes."""
|
||||
try:
|
||||
self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access
|
||||
self._evo_device._set_status(op_mode) # noqa: E501; pylint: disable=protected-access
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
self._handle_exception(err)
|
||||
_handle_exception(err)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode for the TCS.
|
||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set an operating mode for the Controller."""
|
||||
self._set_operation_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
||||
|
||||
Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away'
|
||||
mode is needed, it can be enabled via turn_away_mode_on method.
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set a new preset mode.
|
||||
|
||||
If preset_mode is None, then revert to 'Auto' mode.
|
||||
"""
|
||||
self._set_operation_mode(HA_STATE_TO_TCS.get(operation_mode))
|
||||
self._set_operation_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO))
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on.
|
||||
|
||||
The evohome Controller will not remember is previous operating mode.
|
||||
"""
|
||||
self._set_operation_mode(EVO_AWAY)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off.
|
||||
|
||||
The evohome Controller can not recall its previous operating mode (as
|
||||
intimated by the HA schema), so this method is achieved by setting the
|
||||
Controller's mode back to Auto.
|
||||
"""
|
||||
self._set_operation_mode(EVO_AUTO)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state data of the entire evohome Location.
|
||||
|
||||
This includes state data for the Controller and all its child devices,
|
||||
such as the operating mode of the Controller and the current temp of
|
||||
its children (e.g. Zones, DHW controller).
|
||||
"""
|
||||
# should the latest evohome state data be retreived this cycle?
|
||||
timeout = datetime.now() + timedelta(seconds=55)
|
||||
expired = timeout > self._timers['statusUpdated'] + \
|
||||
self._params[CONF_SCAN_INTERVAL]
|
||||
|
||||
if not expired:
|
||||
return
|
||||
|
||||
# Retrieve the latest state data via the client API
|
||||
loc_idx = self._params[CONF_LOCATION_IDX]
|
||||
|
||||
try:
|
||||
self._status.update(
|
||||
self._client.locations[loc_idx].status()[GWS][0][TCS][0])
|
||||
except (requests.exceptions.RequestException,
|
||||
evohomeclient2.AuthenticationError) as err:
|
||||
self._handle_exception(err)
|
||||
else:
|
||||
self._timers['statusUpdated'] = datetime.now()
|
||||
self._available = True
|
||||
|
||||
_LOGGER.debug("Status = %s", self._status)
|
||||
|
||||
# inform the child devices that state data has been updated
|
||||
pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD}
|
||||
dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt)
|
||||
def update(self) -> None:
|
||||
"""Get the latest state data."""
|
||||
pass
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
"""Provides the constants needed for evohome."""
|
||||
|
||||
"""Support for (EMEA/EU-based) Honeywell TCC climate systems."""
|
||||
DOMAIN = 'evohome'
|
||||
DATA_EVOHOME = 'data_' + DOMAIN
|
||||
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
|
||||
|
||||
# These are used only to help prevent E501 (line too long) violations.
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = DOMAIN
|
||||
|
||||
# The Parent's (i.e. TCS, Controller's) operating mode is one of:
|
||||
EVO_RESET = 'AutoWithReset'
|
||||
EVO_AUTO = 'Auto'
|
||||
EVO_AUTOECO = 'AutoWithEco'
|
||||
EVO_AWAY = 'Away'
|
||||
EVO_DAYOFF = 'DayOff'
|
||||
EVO_CUSTOM = 'Custom'
|
||||
EVO_HEATOFF = 'HeatingOff'
|
||||
|
||||
# The Childs' operating mode is one of:
|
||||
EVO_FOLLOW = 'FollowSchedule' # the operating mode is 'inherited' from the TCS
|
||||
EVO_TEMPOVER = 'TemporaryOverride'
|
||||
EVO_PERMOVER = 'PermanentOverride'
|
||||
|
||||
# These are used only to help prevent E501 (line too long) violations
|
||||
GWS = 'gateways'
|
||||
TCS = 'temperatureControlSystems'
|
||||
|
||||
EVO_STRFTIME = '%Y-%m-%dT%H:%M:%SZ'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Evohome",
|
||||
"documentation": "https://www.home-assistant.io/components/evohome",
|
||||
"requirements": [
|
||||
"evohomeclient==0.3.2"
|
||||
"evohomeclient==0.3.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@zxdavb"]
|
||||
|
|
|
@ -1,90 +1,87 @@
|
|||
"""Support for Fibaro thermostats."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
STATE_AUTO, STATE_COOL, STATE_DRY,
|
||||
STATE_ECO, STATE_FAN_ONLY, STATE_HEAT,
|
||||
STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE)
|
||||
HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice)
|
||||
from . import FIBARO_DEVICES, FibaroDevice
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_OFF,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT)
|
||||
|
||||
from . import (
|
||||
FIBARO_DEVICES, FibaroDevice)
|
||||
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
# State definitions missing from HA, but defined by Z-Wave standard.
|
||||
# We map them to states known supported by HA here:
|
||||
STATE_AUXILIARY = STATE_HEAT
|
||||
STATE_RESUME = STATE_HEAT
|
||||
STATE_MOIST = STATE_DRY
|
||||
STATE_AUTO_CHANGEOVER = STATE_AUTO
|
||||
STATE_ENERGY_HEAT = STATE_ECO
|
||||
STATE_ENERGY_COOL = STATE_COOL
|
||||
STATE_FULL_POWER = STATE_AUTO
|
||||
STATE_FORCE_OPEN = STATE_MANUAL
|
||||
STATE_AWAY = STATE_AUTO
|
||||
STATE_FURNACE = STATE_HEAT
|
||||
|
||||
FAN_AUTO_HIGH = 'auto_high'
|
||||
FAN_AUTO_MEDIUM = 'auto_medium'
|
||||
FAN_CIRCULATION = 'circulation'
|
||||
FAN_HUMIDITY_CIRCULATION = 'humidity_circulation'
|
||||
FAN_LEFT_RIGHT = 'left_right'
|
||||
FAN_UP_DOWN = 'up_down'
|
||||
FAN_QUIET = 'quiet'
|
||||
PRESET_RESUME = 'resume'
|
||||
PRESET_MOIST = 'moist'
|
||||
PRESET_FURNACE = 'furnace'
|
||||
PRESET_CHANGEOVER = 'changeover'
|
||||
PRESET_ECO_HEAT = 'eco_heat'
|
||||
PRESET_ECO_COOL = 'eco_cool'
|
||||
PRESET_FORCE_OPEN = 'force_open'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04
|
||||
# Table 128, Thermostat Fan Mode Set version 4::Fan Mode encoding
|
||||
FANMODES = {
|
||||
0: STATE_OFF,
|
||||
1: SPEED_LOW,
|
||||
2: FAN_AUTO_HIGH,
|
||||
3: SPEED_HIGH,
|
||||
4: FAN_AUTO_MEDIUM,
|
||||
5: SPEED_MEDIUM,
|
||||
6: FAN_CIRCULATION,
|
||||
7: FAN_HUMIDITY_CIRCULATION,
|
||||
8: FAN_LEFT_RIGHT,
|
||||
9: FAN_UP_DOWN,
|
||||
10: FAN_QUIET,
|
||||
128: STATE_AUTO
|
||||
0: 'off',
|
||||
1: 'low',
|
||||
2: 'auto_high',
|
||||
3: 'medium',
|
||||
4: 'auto_medium',
|
||||
5: 'high',
|
||||
6: 'circulation',
|
||||
7: 'humidity_circulation',
|
||||
8: 'left_right',
|
||||
9: 'up_down',
|
||||
10: 'quiet',
|
||||
128: 'auto'
|
||||
}
|
||||
|
||||
HA_FANMODES = {v: k for k, v in FANMODES.items()}
|
||||
|
||||
# SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04
|
||||
# Table 130, Thermostat Mode Set version 3::Mode encoding.
|
||||
OPMODES = {
|
||||
0: STATE_OFF,
|
||||
1: STATE_HEAT,
|
||||
2: STATE_COOL,
|
||||
3: STATE_AUTO,
|
||||
4: STATE_AUXILIARY,
|
||||
5: STATE_RESUME,
|
||||
6: STATE_FAN_ONLY,
|
||||
7: STATE_FURNACE,
|
||||
8: STATE_DRY,
|
||||
9: STATE_MOIST,
|
||||
10: STATE_AUTO_CHANGEOVER,
|
||||
11: STATE_ENERGY_HEAT,
|
||||
12: STATE_ENERGY_COOL,
|
||||
13: STATE_AWAY,
|
||||
15: STATE_FULL_POWER,
|
||||
31: STATE_FORCE_OPEN
|
||||
# 4 AUXILARY
|
||||
OPMODES_PRESET = {
|
||||
5: PRESET_RESUME,
|
||||
7: PRESET_FURNACE,
|
||||
9: PRESET_MOIST,
|
||||
10: PRESET_CHANGEOVER,
|
||||
11: PRESET_ECO_HEAT,
|
||||
12: PRESET_ECO_COOL,
|
||||
13: PRESET_AWAY,
|
||||
15: PRESET_BOOST,
|
||||
31: PRESET_FORCE_OPEN,
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
|
||||
HA_OPMODES_PRESET = {v: k for k, v in OPMODES_PRESET.items()}
|
||||
|
||||
OPMODES_HVAC = {
|
||||
0: HVAC_MODE_OFF,
|
||||
1: HVAC_MODE_HEAT,
|
||||
2: HVAC_MODE_COOL,
|
||||
3: HVAC_MODE_AUTO,
|
||||
4: HVAC_MODE_HEAT,
|
||||
5: HVAC_MODE_AUTO,
|
||||
6: HVAC_MODE_FAN_ONLY,
|
||||
7: HVAC_MODE_HEAT,
|
||||
8: HVAC_MODE_DRY,
|
||||
9: HVAC_MODE_DRY,
|
||||
10: HVAC_MODE_AUTO,
|
||||
11: HVAC_MODE_HEAT,
|
||||
12: HVAC_MODE_COOL,
|
||||
13: HVAC_MODE_AUTO,
|
||||
15: HVAC_MODE_AUTO,
|
||||
31: HVAC_MODE_HEAT,
|
||||
}
|
||||
|
||||
HA_OPMODES_HVAC = {
|
||||
HVAC_MODE_OFF: 0,
|
||||
HVAC_MODE_HEAT: 1,
|
||||
HVAC_MODE_COOL: 2,
|
||||
HVAC_MODE_AUTO: 3,
|
||||
HVAC_MODE_FAN_ONLY: 6,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
@ -109,10 +106,9 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
self._fan_mode_device = None
|
||||
self._support_flags = 0
|
||||
self.entity_id = 'climate.{}'.format(self.ha_id)
|
||||
self._fan_mode_to_state = {}
|
||||
self._fan_state_to_mode = {}
|
||||
self._op_mode_to_state = {}
|
||||
self._op_state_to_mode = {}
|
||||
self._hvac_support = []
|
||||
self._preset_support = []
|
||||
self._fan_support = []
|
||||
|
||||
siblings = fibaro_device.fibaro_controller.get_siblings(
|
||||
fibaro_device.id)
|
||||
|
@ -129,7 +125,7 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
if 'setMode' in device.actions or \
|
||||
'setOperatingMode' in device.actions:
|
||||
self._op_mode_device = FibaroDevice(device)
|
||||
self._support_flags |= SUPPORT_OPERATION_MODE
|
||||
self._support_flags |= SUPPORT_PRESET_MODE
|
||||
if 'setFanMode' in device.actions:
|
||||
self._fan_mode_device = FibaroDevice(device)
|
||||
self._support_flags |= SUPPORT_FAN_MODE
|
||||
|
@ -143,11 +139,11 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
fan_modes = self._fan_mode_device.fibaro_device.\
|
||||
properties.supportedModes.split(",")
|
||||
for mode in fan_modes:
|
||||
try:
|
||||
self._fan_mode_to_state[int(mode)] = FANMODES[int(mode)]
|
||||
self._fan_state_to_mode[FANMODES[int(mode)]] = int(mode)
|
||||
except KeyError:
|
||||
self._fan_mode_to_state[int(mode)] = 'unknown'
|
||||
mode = int(mode)
|
||||
if mode not in FANMODES:
|
||||
_LOGGER.warning("%d unknown fan mode", mode)
|
||||
continue
|
||||
self._fan_support.append(FANMODES[int(mode)])
|
||||
|
||||
if self._op_mode_device:
|
||||
prop = self._op_mode_device.fibaro_device.properties
|
||||
|
@ -156,11 +152,13 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
elif "supportedModes" in prop:
|
||||
op_modes = prop.supportedModes.split(",")
|
||||
for mode in op_modes:
|
||||
try:
|
||||
self._op_mode_to_state[int(mode)] = OPMODES[int(mode)]
|
||||
self._op_state_to_mode[OPMODES[int(mode)]] = int(mode)
|
||||
except KeyError:
|
||||
self._op_mode_to_state[int(mode)] = 'unknown'
|
||||
mode = int(mode)
|
||||
if mode in OPMODES_HVAC:
|
||||
mode_ha = OPMODES_HVAC[mode]
|
||||
if mode_ha not in self._hvac_support:
|
||||
self._hvac_support.append(mode_ha)
|
||||
if mode in OPMODES_PRESET:
|
||||
self._preset_support.append(OPMODES_PRESET[mode])
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
|
@ -194,32 +192,70 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
return self._support_flags
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
if self._fan_mode_device is None:
|
||||
if not self._fan_mode_device:
|
||||
return None
|
||||
return list(self._fan_state_to_mode)
|
||||
return self._fan_support
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
if self._fan_mode_device is None:
|
||||
if not self._fan_mode_device:
|
||||
return None
|
||||
|
||||
mode = int(self._fan_mode_device.fibaro_device.properties.mode)
|
||||
return self._fan_mode_to_state[mode]
|
||||
return FANMODES[mode]
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if self._fan_mode_device is None:
|
||||
if not self._fan_mode_device:
|
||||
return
|
||||
self._fan_mode_device.action(
|
||||
"setFanMode", self._fan_state_to_mode[fan_mode])
|
||||
self._fan_mode_device.action("setFanMode", HA_FANMODES[fan_mode])
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def fibaro_op_mode(self):
|
||||
"""Return the operating mode of the device."""
|
||||
if not self._op_mode_device:
|
||||
return 6 # Fan only
|
||||
|
||||
if "operatingMode" in self._op_mode_device.fibaro_device.properties:
|
||||
return int(self._op_mode_device.fibaro_device.
|
||||
properties.operatingMode)
|
||||
|
||||
return int(self._op_mode_device.fibaro_device.properties.mode)
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._op_mode_device is None:
|
||||
return OPMODES_HVAC[self.fibaro_op_mode]
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
if not self._op_mode_device:
|
||||
return [HVAC_MODE_FAN_ONLY]
|
||||
return self._hvac_support
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target operation mode."""
|
||||
if not self._op_mode_device:
|
||||
return
|
||||
if self.preset_mode:
|
||||
return
|
||||
|
||||
if "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
|
||||
self._op_mode_device.action(
|
||||
"setOperatingMode", HA_OPMODES_HVAC[hvac_mode])
|
||||
elif "setMode" in self._op_mode_device.fibaro_device.actions:
|
||||
self._op_mode_device.action("setMode", HA_OPMODES_HVAC[hvac_mode])
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
if not self._op_mode_device:
|
||||
return None
|
||||
|
||||
if "operatingMode" in self._op_mode_device.fibaro_device.properties:
|
||||
|
@ -227,25 +263,31 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
properties.operatingMode)
|
||||
else:
|
||||
mode = int(self._op_mode_device.fibaro_device.properties.mode)
|
||||
return self._op_mode_to_state.get(mode)
|
||||
|
||||
if mode not in OPMODES_PRESET:
|
||||
return None
|
||||
return OPMODES_PRESET[mode]
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
if self._op_mode_device is None:
|
||||
return None
|
||||
return list(self._op_state_to_mode)
|
||||
def preset_modes(self):
|
||||
"""Return a list of available preset modes.
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
Requires SUPPORT_PRESET_MODE.
|
||||
"""
|
||||
if not self._op_mode_device:
|
||||
return None
|
||||
return self._preset_support
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self._op_mode_device is None:
|
||||
return
|
||||
if "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
|
||||
self._op_mode_device.action(
|
||||
"setOperatingMode", self._op_state_to_mode[operation_mode])
|
||||
"setOperatingMode", HA_OPMODES_PRESET[preset_mode])
|
||||
elif "setMode" in self._op_mode_device.fibaro_device.actions:
|
||||
self._op_mode_device.action(
|
||||
"setMode", self._op_state_to_mode[operation_mode])
|
||||
"setMode", HA_OPMODES_PRESET[preset_mode])
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
|
@ -275,15 +317,6 @@ class FibaroThermostat(FibaroDevice, ClimateDevice):
|
|||
if temperature is not None:
|
||||
if "setThermostatSetpoint" in target.fibaro_device.actions:
|
||||
target.action("setThermostatSetpoint",
|
||||
self._op_state_to_mode[self.current_operation],
|
||||
temperature)
|
||||
self.fibaro_op_mode, temperature)
|
||||
else:
|
||||
target.action("setTargetLevel",
|
||||
temperature)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
if self.current_operation == STATE_OFF:
|
||||
return False
|
||||
return True
|
||||
target.action("setTargetLevel", temperature)
|
||||
|
|
|
@ -12,6 +12,7 @@ For more details about this platform, please refer to the documentation
|
|||
https://home-assistant.io/components/climate.flexit/
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
|
@ -20,7 +21,7 @@ from homeassistant.const import (
|
|||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_FAN_MODE)
|
||||
SUPPORT_FAN_MODE, HVAC_MODE_COOL)
|
||||
from homeassistant.components.modbus import (
|
||||
CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -57,7 +58,7 @@ class Flexit(ClimateDevice):
|
|||
self._current_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._fan_list = ['Off', 'Low', 'Medium', 'High']
|
||||
self._fan_modes = ['Off', 'Low', 'Medium', 'High']
|
||||
self._current_operation = None
|
||||
self._filter_hours = None
|
||||
self._filter_alarm = None
|
||||
|
@ -81,7 +82,7 @@ class Flexit(ClimateDevice):
|
|||
self._target_temperature = self.unit.get_target_temp
|
||||
self._current_temperature = self.unit.get_temp
|
||||
self._current_fan_mode =\
|
||||
self._fan_list[self.unit.get_fan_speed]
|
||||
self._fan_modes[self.unit.get_fan_speed]
|
||||
self._filter_hours = self.unit.get_filter_hours
|
||||
# Mechanical heat recovery, 0-100%
|
||||
self._heat_recovery = self.unit.get_heat_recovery
|
||||
|
@ -134,19 +135,27 @@ class Flexit(ClimateDevice):
|
|||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
def hvac_modes(self) -> List[str]:
|
||||
"""Return the list of available hvac operation modes.
|
||||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
return [HVAC_MODE_COOL]
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
return self._fan_modes
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
@ -156,4 +165,4 @@ class Flexit(ClimateDevice):
|
|||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan_mode))
|
||||
self.unit.set_fan_speed(self._fan_modes.index(fan_mode))
|
||||
|
|
|
@ -5,11 +5,11 @@ import requests
|
|||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_OPERATION_MODE, STATE_ECO, STATE_HEAT, STATE_MANUAL,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
ATTR_HVAC_MODE, HVAC_MODE_HEAT, PRESET_ECO, PRESET_COMFORT,
|
||||
SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, SUPPORT_PRESET_MODE)
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF,
|
||||
STATE_ON, TEMP_CELSIUS)
|
||||
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
from . import (
|
||||
ATTR_STATE_BATTERY_LOW, ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_HOLIDAY_MODE,
|
||||
|
@ -18,13 +18,15 @@ from . import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON]
|
||||
OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
MIN_TEMPERATURE = 8
|
||||
MAX_TEMPERATURE = 28
|
||||
|
||||
PRESET_MANUAL = 'manual'
|
||||
|
||||
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
|
||||
ON_API_TEMPERATURE = 127.0
|
||||
OFF_API_TEMPERATURE = 126.5
|
||||
|
@ -98,41 +100,51 @@ class FritzboxThermostat(ClimateDevice):
|
|||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_OPERATION_MODE in kwargs:
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
self.set_operation_mode(operation_mode)
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
self.set_hvac_mode(hvac_mode)
|
||||
elif ATTR_TEMPERATURE in kwargs:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._device.set_target_temperature(temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_mode(self):
|
||||
"""Return the current operation mode."""
|
||||
if self._target_temperature == ON_API_TEMPERATURE:
|
||||
return STATE_ON
|
||||
if self._target_temperature == OFF_API_TEMPERATURE:
|
||||
return STATE_OFF
|
||||
if self._target_temperature == self._comfort_temperature:
|
||||
return STATE_HEAT
|
||||
if self._target_temperature == self._eco_temperature:
|
||||
return STATE_ECO
|
||||
return STATE_MANUAL
|
||||
if self._target_temperature == OFF_REPORT_SET_TEMPERATURE:
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
return HVAC_MODE_HEAT
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.set_temperature(temperature=self._eco_temperature)
|
||||
elif operation_mode == STATE_OFF:
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
elif operation_mode == STATE_ON:
|
||||
self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
else:
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
if self._target_temperature == self._comfort_temperature:
|
||||
return PRESET_COMFORT
|
||||
if self._target_temperature == self._eco_temperature:
|
||||
return PRESET_ECO
|
||||
|
||||
def preset_modes(self):
|
||||
"""Return supported preset modes."""
|
||||
return [PRESET_ECO, PRESET_COMFORT]
|
||||
|
||||
def set_preset_mode(self, preset_mode):
|
||||
"""Set preset mode."""
|
||||
if preset_mode == PRESET_COMFORT:
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
elif preset_mode == PRESET_ECO:
|
||||
self.set_temperature(temperature=self._eco_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
|
1
homeassistant/components/fronius/__init__.py
Normal file
1
homeassistant/components/fronius/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""The Fronius component."""
|
8
homeassistant/components/fronius/manifest.json
Normal file
8
homeassistant/components/fronius/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"domain": "fronius",
|
||||
"name": "Fronius",
|
||||
"documentation": "https://www.home-assistant.io/components/fronius",
|
||||
"requirements": ["pyfronius==0.4.6"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@nielstron"]
|
||||
}
|
197
homeassistant/components/fronius/sensor.py
Normal file
197
homeassistant/components/fronius/sensor.py
Normal file
|
@ -0,0 +1,197 @@
|
|||
"""Support for Fronius devices."""
|
||||
import copy
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from pyfronius import Fronius
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (CONF_RESOURCE, CONF_SENSOR_TYPE, CONF_DEVICE,
|
||||
CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SCOPE = 'scope'
|
||||
|
||||
TYPE_INVERTER = 'inverter'
|
||||
TYPE_STORAGE = 'storage'
|
||||
TYPE_METER = 'meter'
|
||||
TYPE_POWER_FLOW = 'power_flow'
|
||||
SCOPE_DEVICE = 'device'
|
||||
SCOPE_SYSTEM = 'system'
|
||||
|
||||
DEFAULT_SCOPE = SCOPE_DEVICE
|
||||
DEFAULT_DEVICE = 0
|
||||
DEFAULT_INVERTER = 1
|
||||
|
||||
SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW]
|
||||
SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM]
|
||||
|
||||
|
||||
def _device_id_validator(config):
|
||||
"""Ensure that inverters have default id 1 and other devices 0."""
|
||||
config = copy.deepcopy(config)
|
||||
for cond in config[CONF_MONITORED_CONDITIONS]:
|
||||
if CONF_DEVICE not in cond:
|
||||
if cond[CONF_SENSOR_TYPE] == TYPE_INVERTER:
|
||||
cond[CONF_DEVICE] = DEFAULT_INVERTER
|
||||
else:
|
||||
cond[CONF_DEVICE] = DEFAULT_DEVICE
|
||||
return config
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE):
|
||||
cv.url,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(
|
||||
cv.ensure_list,
|
||||
[{
|
||||
vol.Required(CONF_SENSOR_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(CONF_SCOPE, default=DEFAULT_SCOPE):
|
||||
vol.In(SCOPE_TYPES),
|
||||
vol.Optional(CONF_DEVICE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
}]
|
||||
)
|
||||
}), _device_id_validator))
|
||||
|
||||
|
||||
async def async_setup_platform(hass,
|
||||
config,
|
||||
async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up of Fronius platform."""
|
||||
session = async_get_clientsession(hass)
|
||||
fronius = Fronius(session, config[CONF_RESOURCE])
|
||||
|
||||
sensors = []
|
||||
for condition in config[CONF_MONITORED_CONDITIONS]:
|
||||
|
||||
device = condition[CONF_DEVICE]
|
||||
name = "Fronius {} {} {}".format(
|
||||
condition[CONF_SENSOR_TYPE].replace('_', ' ').capitalize(),
|
||||
device,
|
||||
config[CONF_RESOURCE],
|
||||
)
|
||||
sensor_type = condition[CONF_SENSOR_TYPE]
|
||||
scope = condition[CONF_SCOPE]
|
||||
if sensor_type == TYPE_INVERTER:
|
||||
if scope == SCOPE_SYSTEM:
|
||||
sensor_cls = FroniusInverterSystem
|
||||
else:
|
||||
sensor_cls = FroniusInverterDevice
|
||||
elif sensor_type == TYPE_METER:
|
||||
if scope == SCOPE_SYSTEM:
|
||||
sensor_cls = FroniusMeterSystem
|
||||
else:
|
||||
sensor_cls = FroniusMeterDevice
|
||||
elif sensor_type == TYPE_POWER_FLOW:
|
||||
sensor_cls = FroniusPowerFlow
|
||||
else:
|
||||
sensor_cls = FroniusStorage
|
||||
|
||||
sensors.append(sensor_cls(fronius, name, device))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class FroniusSensor(Entity):
|
||||
"""The Fronius sensor implementation."""
|
||||
|
||||
def __init__(self, data, name, device):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._state = None
|
||||
self._attributes = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attributes
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve and update latest state."""
|
||||
values = {}
|
||||
try:
|
||||
values = await self._update()
|
||||
except ConnectionError:
|
||||
_LOGGER.error("Failed to update: connection error")
|
||||
except ValueError:
|
||||
_LOGGER.error("Failed to update: invalid response returned."
|
||||
"Maybe the configured device is not supported")
|
||||
|
||||
if values:
|
||||
self._state = values['status']['Code']
|
||||
attributes = {}
|
||||
for key in values:
|
||||
if 'value' in values[key]:
|
||||
attributes[key] = values[key].get('value', 0)
|
||||
self._attributes = attributes
|
||||
|
||||
async def _update(self):
|
||||
"""Return values of interest."""
|
||||
pass
|
||||
|
||||
|
||||
class FroniusInverterSystem(FroniusSensor):
|
||||
"""Sensor for the fronius inverter with system scope."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_system_inverter_data()
|
||||
|
||||
|
||||
class FroniusInverterDevice(FroniusSensor):
|
||||
"""Sensor for the fronius inverter with device scope."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_inverter_data(self._device)
|
||||
|
||||
|
||||
class FroniusStorage(FroniusSensor):
|
||||
"""Sensor for the fronius battery storage."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_storage_data(self._device)
|
||||
|
||||
|
||||
class FroniusMeterSystem(FroniusSensor):
|
||||
"""Sensor for the fronius meter with system scope."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_system_meter_data()
|
||||
|
||||
|
||||
class FroniusMeterDevice(FroniusSensor):
|
||||
"""Sensor for the fronius meter with device scope."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_meter_data(self._device)
|
||||
|
||||
|
||||
class FroniusPowerFlow(FroniusSensor):
|
||||
"""Sensor for the fronius power flow."""
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.data.current_power_flow()
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue