Merge commit 'df920b4eda1d64368ed3bf166bcb0a90aeec6c44' into rc

This commit is contained in:
Paulus Schoutsen 2019-07-10 20:54:06 -07:00
commit 87d3680630
455 changed files with 16339 additions and 8596 deletions

View file

@ -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
View 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

View 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"
}
}

View file

@ -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!
-->

View file

@ -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
View 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
View 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
View file

@ -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
View 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": []
}
]
}

View file

@ -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

View file

@ -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" ]

View file

@ -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'

View file

@ -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'

View file

@ -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()

View file

@ -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):

View file

@ -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": {

View file

@ -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"
}
}

View file

@ -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": {

View file

@ -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"
},

View file

@ -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": {

View file

@ -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": {

View file

@ -1,6 +1,7 @@
{
"config": {
"abort": {
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
"single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt."
},
"error": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -1,6 +1,7 @@
{
"config": {
"abort": {
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.",
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
},
"error": {

View file

@ -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": {

View file

@ -4,5 +4,8 @@
"documentation": "https://www.home-assistant.io/components/alert",
"requirements": [],
"dependencies": [],
"after_dependencies": [
"notify"
],
"codeowners": []
}

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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',

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""

View file

@ -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

View file

@ -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": []

View file

@ -0,0 +1,5 @@
{
"config": {
"title": "Arcam FMJ"
}
}

View file

@ -0,0 +1,5 @@
{
"config": {
"title": "Arcam FMJ"
}
}

View file

@ -0,0 +1,5 @@
{
"config": {
"title": "Arcam FMJ"
}
}

View file

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"one": "Een",
"other": "Ander"
},
"error": {
"one": "Een",
"other": "Ander"
},
"step": {
"one": "Een",
"other": "Ander"
},
"title": "Arcam FMJ"
}
}

View 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"
}
}

View file

@ -0,0 +1,5 @@
{
"config": {
"title": "Arcam FMJ"
}
}

View 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

View 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)

View 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)

View 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"
]
}

View 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

View file

@ -0,0 +1,8 @@
{
"config": {
"title": "Arcam FMJ",
"step": {},
"error": {},
"abort": {}
}
}

View file

@ -0,0 +1 @@
"""The Aurora ABB Powerone PV inverter sensor integration."""

View 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"]
}

View 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()

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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())

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 = {}

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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'})

View file

@ -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"
},

View file

@ -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."""

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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": []

View file

@ -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": []

View file

@ -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 \

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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": []

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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'

View file

@ -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"]

View file

@ -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)

View file

@ -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))

View file

@ -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):

View file

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

View 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"]
}

View 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