feat: first commit
commit
838fff399b
@ -0,0 +1,77 @@
|
|||||||
|
name: Bug report / 报告问题
|
||||||
|
description: Create a report to help us improve. / 报告问题以帮助我们改进
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug / 描述问题
|
||||||
|
description: |
|
||||||
|
> A clear and concise description of what the bug is.
|
||||||
|
> 清晰且简明地描述问题。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: To Reproduce / 复现步骤
|
||||||
|
description: |
|
||||||
|
> If applicable, add screenshots to help explain your problem. You can attach images by clicking this area to highlight it and then dragging files in. Steps to reproduce the behavior:
|
||||||
|
> 如有需要,可添加截图以帮助解释问题。点击此区域以高亮显示并拖动截图文件以上传。请详细描述复现步骤:
|
||||||
|
placeholder: |
|
||||||
|
1. Go to ...
|
||||||
|
2. Click on ...
|
||||||
|
3. Scroll down to ...
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior / 预期结果
|
||||||
|
description: |
|
||||||
|
> A clear and concise description of what you expected to happen.
|
||||||
|
> 描述预期结果。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Home Assistant Logs / 系统日志
|
||||||
|
description: |
|
||||||
|
> [Settings > System > Logs > DOWNLOAD FULL LOG](https://my.home-assistant.io/redirect/logs) > Filter `xiaomi_home`
|
||||||
|
> [设置 > 系统 > 日志 > 下载完整日志](https://my.home-assistant.io/redirect/logs) > 筛选 `xiaomi_home`
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Home Assistant Core version / Home Assistant Core 版本
|
||||||
|
description: |
|
||||||
|
> [Settings > About](https://my.home-assistant.io/redirect/info)
|
||||||
|
> [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)
|
||||||
|
placeholder: "2024.8.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Home Assistant Operation System version / Home Assistant Operation System 版本
|
||||||
|
description: |
|
||||||
|
> [Settings > About](https://my.home-assistant.io/redirect/info)
|
||||||
|
> [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)
|
||||||
|
placeholder: "12.4"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Xiaomi Home integration version / 米家集成版本
|
||||||
|
description: |
|
||||||
|
> [Settings > Devices & services > Configured > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||||
|
> [设置 > 设备与服务 > 已配置 > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||||
|
placeholder: "v0.0.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context / 其他说明
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Feature Suggestion / 功能建议
|
||||||
|
url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas
|
||||||
|
about: Share ideas for enhancements or new features. / 建议改进或增加新功能
|
||||||
|
|
||||||
|
- name: Support and Help / 支持与帮助
|
||||||
|
url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/categories/q-a
|
||||||
|
about: Please ask and answer questions here. / 请在这里提问和答疑
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
name: Validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-hassfest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Hassfest validation
|
||||||
|
uses: home-assistant/actions/hassfest@master
|
||||||
|
|
||||||
|
validate-hacs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: HACS validation
|
||||||
|
uses: hacs/action@main
|
||||||
|
with:
|
||||||
|
category: integration
|
||||||
|
ignore: brands
|
||||||
|
|
||||||
|
validate-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check format
|
||||||
|
run: |
|
||||||
|
./custom_components/xiaomi_home/test/test_all.sh
|
||||||
|
|
||||||
|
validate-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pylint
|
||||||
|
|
||||||
|
- name: Analyse the code with pylint
|
||||||
|
run: |
|
||||||
|
pylint $(git ls-files '*.py')
|
||||||
|
|
||||||
|
validate-setup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install the integration
|
||||||
|
run: |
|
||||||
|
export config_path=./test_config
|
||||||
|
mkdir $config_path
|
||||||
|
./install.sh $config_path
|
||||||
|
echo "default_config:" >> $config_path/configuration.yaml
|
||||||
|
echo "logger:" >> $config_path/configuration.yaml
|
||||||
|
echo " default: info" >> $config_path/configuration.yaml
|
||||||
|
echo " logs:" >> $config_path/configuration.yaml
|
||||||
|
echo " custom_components.xiaomi_home: debug" >> $config_path/configuration.yaml
|
||||||
|
|
||||||
|
- name: Setup Home Assistant
|
||||||
|
id: homeassistant
|
||||||
|
uses: ludeeus/setup-homeassistant@main
|
||||||
|
with:
|
||||||
|
config-dir: ./test_config
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
requirements.txt
|
||||||
@ -0,0 +1,398 @@
|
|||||||
|
# This Pylint rcfile contains a best-effort configuration to uphold the
|
||||||
|
# best-practices and style described in the Google Python style guide:
|
||||||
|
# https://google.github.io/styleguide/pyguide.html
|
||||||
|
#
|
||||||
|
# Its canonical open-source location is:
|
||||||
|
# https://google.github.io/styleguide/pylintrc
|
||||||
|
|
||||||
|
[MAIN]
|
||||||
|
|
||||||
|
# Files or directories to be skipped. They should be base names, not paths.
|
||||||
|
ignore=third_party
|
||||||
|
|
||||||
|
# Files or directories matching the regex patterns are skipped. The regex
|
||||||
|
# matches against base names, not paths.
|
||||||
|
ignore-patterns=
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=no
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
# Use multiple processes to speed up Pylint.
|
||||||
|
jobs=4
|
||||||
|
|
||||||
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||||
|
# active Python interpreter and may run arbitrary code.
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||||
|
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||||
|
confidence=
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time (only on the command line, not in the configuration file where
|
||||||
|
# it should appear only once). See also the "--disable" option for examples.
|
||||||
|
#enable=
|
||||||
|
|
||||||
|
# Disable the message, report, category or checker with the given id(s). You
|
||||||
|
# can either give multiple identifiers separated by comma (,) or put this
|
||||||
|
# option multiple times (only on the command line, not in the configuration
|
||||||
|
# file where it should appear only once).You can also use "--disable=all" to
|
||||||
|
# disable everything first and then reenable specific checks. For example, if
|
||||||
|
# you want to run only the similarities checker, you can use "--disable=all
|
||||||
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
|
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||||
|
# --disable=W"
|
||||||
|
disable=R,
|
||||||
|
abstract-method,
|
||||||
|
apply-builtin,
|
||||||
|
arguments-differ,
|
||||||
|
attribute-defined-outside-init,
|
||||||
|
backtick,
|
||||||
|
bad-option-value,
|
||||||
|
basestring-builtin,
|
||||||
|
buffer-builtin,
|
||||||
|
c-extension-no-member,
|
||||||
|
consider-using-enumerate,
|
||||||
|
cmp-builtin,
|
||||||
|
cmp-method,
|
||||||
|
coerce-builtin,
|
||||||
|
coerce-method,
|
||||||
|
delslice-method,
|
||||||
|
div-method,
|
||||||
|
eq-without-hash,
|
||||||
|
execfile-builtin,
|
||||||
|
file-builtin,
|
||||||
|
filter-builtin-not-iterating,
|
||||||
|
fixme,
|
||||||
|
getslice-method,
|
||||||
|
global-statement,
|
||||||
|
hex-method,
|
||||||
|
idiv-method,
|
||||||
|
implicit-str-concat,
|
||||||
|
import-error,
|
||||||
|
import-self,
|
||||||
|
import-star-module-level,
|
||||||
|
input-builtin,
|
||||||
|
intern-builtin,
|
||||||
|
invalid-str-codec,
|
||||||
|
locally-disabled,
|
||||||
|
long-builtin,
|
||||||
|
long-suffix,
|
||||||
|
map-builtin-not-iterating,
|
||||||
|
misplaced-comparison-constant,
|
||||||
|
missing-function-docstring,
|
||||||
|
metaclass-assignment,
|
||||||
|
next-method-called,
|
||||||
|
next-method-defined,
|
||||||
|
no-absolute-import,
|
||||||
|
no-init, # added
|
||||||
|
no-member,
|
||||||
|
no-name-in-module,
|
||||||
|
no-self-use,
|
||||||
|
nonzero-method,
|
||||||
|
oct-method,
|
||||||
|
old-division,
|
||||||
|
old-ne-operator,
|
||||||
|
old-octal-literal,
|
||||||
|
old-raise-syntax,
|
||||||
|
parameter-unpacking,
|
||||||
|
print-statement,
|
||||||
|
raising-string,
|
||||||
|
range-builtin-not-iterating,
|
||||||
|
raw_input-builtin,
|
||||||
|
rdiv-method,
|
||||||
|
reduce-builtin,
|
||||||
|
relative-import,
|
||||||
|
reload-builtin,
|
||||||
|
round-builtin,
|
||||||
|
setslice-method,
|
||||||
|
signature-differs,
|
||||||
|
standarderror-builtin,
|
||||||
|
suppressed-message,
|
||||||
|
sys-max-int,
|
||||||
|
trailing-newlines,
|
||||||
|
unichr-builtin,
|
||||||
|
unicode-builtin,
|
||||||
|
unnecessary-pass,
|
||||||
|
unpacking-in-except,
|
||||||
|
useless-else-on-loop,
|
||||||
|
useless-suppression,
|
||||||
|
using-cmp-argument,
|
||||||
|
wrong-import-order,
|
||||||
|
xrange-builtin,
|
||||||
|
zip-builtin-not-iterating,
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||||
|
# (visual studio) and html. You can also give a reporter class, eg
|
||||||
|
# mypackage.mymodule.MyReporterClass.
|
||||||
|
output-format=text
|
||||||
|
|
||||||
|
# Tells whether to display a full report or only the messages
|
||||||
|
reports=no
|
||||||
|
|
||||||
|
# Python expression which should return a note less than 10 (10 is the highest
|
||||||
|
# note). You have access to the variables errors warning, statement which
|
||||||
|
# respectively contain the number of errors / warnings messages and the total
|
||||||
|
# number of statements analyzed. This is used by the global evaluation report
|
||||||
|
# (RP0004).
|
||||||
|
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||||
|
|
||||||
|
# Template used to display messages. This is a python new-style format string
|
||||||
|
# used to format the message information. See doc for all details
|
||||||
|
#msg-template=
|
||||||
|
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
|
||||||
|
# Good variable names which should always be accepted, separated by a comma
|
||||||
|
good-names=main,_
|
||||||
|
|
||||||
|
# Bad variable names which should always be refused, separated by a comma
|
||||||
|
bad-names=
|
||||||
|
|
||||||
|
# Colon-delimited sets of names that determine each other's naming style when
|
||||||
|
# the name regexes allow several styles.
|
||||||
|
name-group=
|
||||||
|
|
||||||
|
# Include a hint for the correct naming format with invalid-name
|
||||||
|
include-naming-hint=no
|
||||||
|
|
||||||
|
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||||
|
# to this list to register other decorators that produce valid properties.
|
||||||
|
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
|
||||||
|
|
||||||
|
# Regular expression matching correct function names
|
||||||
|
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
|
||||||
|
|
||||||
|
# Regular expression matching correct variable names
|
||||||
|
variable-rgx=^[a-z][a-z0-9_]*$
|
||||||
|
|
||||||
|
# Regular expression matching correct constant names
|
||||||
|
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
|
||||||
|
|
||||||
|
# Regular expression matching correct attribute names
|
||||||
|
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
|
||||||
|
|
||||||
|
# Regular expression matching correct argument names
|
||||||
|
argument-rgx=^[a-z][a-z0-9_]*$
|
||||||
|
|
||||||
|
# Regular expression matching correct class attribute names
|
||||||
|
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
|
||||||
|
|
||||||
|
# Regular expression matching correct inline iteration names
|
||||||
|
inlinevar-rgx=^[a-z][a-z0-9_]*$
|
||||||
|
|
||||||
|
# Regular expression matching correct class names
|
||||||
|
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
|
||||||
|
|
||||||
|
# Regular expression matching correct module names
|
||||||
|
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
|
||||||
|
|
||||||
|
# Regular expression matching correct method names
|
||||||
|
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
|
||||||
|
|
||||||
|
# Regular expression which should only match function or class names that do
|
||||||
|
# not require a docstring.
|
||||||
|
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
|
||||||
|
|
||||||
|
# Minimum line length for functions/classes that require docstrings, shorter
|
||||||
|
# ones are exempt.
|
||||||
|
docstring-min-length=12
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# List of decorators that produce context managers, such as
|
||||||
|
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||||
|
# produce valid context managers.
|
||||||
|
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
|
||||||
|
|
||||||
|
# List of module names for which member attributes should not be checked
|
||||||
|
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||||
|
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||||
|
# supports qualified module names, as well as Unix pattern matching.
|
||||||
|
ignored-modules=
|
||||||
|
|
||||||
|
# List of class names for which member attributes should not be checked (useful
|
||||||
|
# for classes with dynamically set attributes). This supports the use of
|
||||||
|
# qualified names.
|
||||||
|
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||||
|
|
||||||
|
# List of members which are set dynamically and missed by pylint inference
|
||||||
|
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||||
|
# expressions are accepted.
|
||||||
|
generated-members=
|
||||||
|
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=80
|
||||||
|
|
||||||
|
# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt
|
||||||
|
# lines made too long by directives to pytype.
|
||||||
|
|
||||||
|
# Regexp for a line that is allowed to be longer than the limit.
|
||||||
|
ignore-long-lines=(?x)(
|
||||||
|
^\s*(\#\ )?<?https?://\S+>?$|
|
||||||
|
^\s*(from\s+\S+\s+)?import\s+.+$)
|
||||||
|
|
||||||
|
# Allow the body of an if to be on the same line as the test if there is no
|
||||||
|
# else.
|
||||||
|
single-line-if-stmt=yes
|
||||||
|
|
||||||
|
# Maximum number of lines in a module
|
||||||
|
max-module-lines=99999
|
||||||
|
|
||||||
|
# String used as indentation unit. The internal Google style guide mandates 2
|
||||||
|
# spaces. Google's externaly-published style guide says 4, consistent with
|
||||||
|
# PEP 8. Here, we use 4 spaces.
|
||||||
|
indent-string=' '
|
||||||
|
|
||||||
|
# Number of spaces of indent required inside a hanging or continued line.
|
||||||
|
indent-after-paren=4
|
||||||
|
|
||||||
|
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||||
|
expected-line-ending-format=
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=TODO
|
||||||
|
|
||||||
|
|
||||||
|
[STRING]
|
||||||
|
|
||||||
|
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||||
|
# character used as a quote delimiter is used inconsistently within a module.
|
||||||
|
check-quote-consistency=yes
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||||
|
# not used).
|
||||||
|
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
|
||||||
|
|
||||||
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
|
# you should avoid to define new builtins when possible.
|
||||||
|
additional-builtins=
|
||||||
|
|
||||||
|
# List of strings which can identify a callback function by name. A callback
|
||||||
|
# name must start or end with one of those strings.
|
||||||
|
callbacks=cb_,_cb
|
||||||
|
|
||||||
|
# List of qualified module names which can have objects that can redefine
|
||||||
|
# builtins.
|
||||||
|
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
|
||||||
|
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
|
||||||
|
# Logging modules to check that the string format arguments are in logging
|
||||||
|
# function parameter format
|
||||||
|
logging-modules=logging,absl.logging,tensorflow.io.logging
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
|
||||||
|
[SPELLING]
|
||||||
|
|
||||||
|
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||||
|
# install python-enchant package.
|
||||||
|
spelling-dict=
|
||||||
|
|
||||||
|
# List of comma separated words that should not be checked.
|
||||||
|
spelling-ignore-words=
|
||||||
|
|
||||||
|
# A path to a file that contains private dictionary; one word per line.
|
||||||
|
spelling-private-dict-file=
|
||||||
|
|
||||||
|
# Tells whether to store unknown words to indicated private dictionary in
|
||||||
|
# --spelling-private-dict-file option instead of raising a message.
|
||||||
|
spelling-store-unknown-words=no
|
||||||
|
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
|
||||||
|
# Deprecated modules which should not be used, separated by a comma
|
||||||
|
deprecated-modules=regsub,
|
||||||
|
TERMIOS,
|
||||||
|
Bastion,
|
||||||
|
rexec,
|
||||||
|
sets
|
||||||
|
|
||||||
|
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||||
|
# given file (report RP0402 must not be disabled)
|
||||||
|
import-graph=
|
||||||
|
|
||||||
|
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
ext-import-graph=
|
||||||
|
|
||||||
|
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
int-import-graph=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of the standard
|
||||||
|
# compatibility libraries.
|
||||||
|
known-standard-library=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of a third party library.
|
||||||
|
known-third-party=enchant, absl
|
||||||
|
|
||||||
|
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||||
|
# 3 compatible code, which means that the block might have code that exists
|
||||||
|
# only in one or another interpreter, leading to false positives when analysed.
|
||||||
|
analyse-fallback-blocks=no
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,
|
||||||
|
__new__,
|
||||||
|
setUp
|
||||||
|
|
||||||
|
# List of member names, which should be excluded from the protected access
|
||||||
|
# warning.
|
||||||
|
exclude-protected=_asdict,
|
||||||
|
_fields,
|
||||||
|
_replace,
|
||||||
|
_source,
|
||||||
|
_make
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls,
|
||||||
|
class_
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=mcs
|
||||||
@ -0,0 +1,394 @@
|
|||||||
|
# Xiaomi Home Integration for Home Assistant
|
||||||
|
|
||||||
|
[English](./README.md) | [简体中文](./doc/README_zh.md)
|
||||||
|
|
||||||
|
Xiaomi Home Integration is an integrated component of Home Assistant supported by Xiaomi official. It allows you to use Xiaomi IoT smart devices in Home Assistant.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
> Home Assistant version requirement:
|
||||||
|
>
|
||||||
|
> - Core $\geq$ 2024.12.1
|
||||||
|
> - Operating System $\geq$ 14.0
|
||||||
|
|
||||||
|
### Method 1: Git clone from GitHub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd config
|
||||||
|
git clone https://github.com/XiaoMi/ha_xiaomi_home.git
|
||||||
|
cd ha_xiaomi_home
|
||||||
|
./install.sh /config
|
||||||
|
```
|
||||||
|
|
||||||
|
We recommend this installation method, for it is convenient to switch to a tag when updating `xiaomi_home` to a certain version.
|
||||||
|
|
||||||
|
For example, update to version v1.0.0
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd config/ha_xiaomi_home
|
||||||
|
git checkout v1.0.0
|
||||||
|
./install.sh /config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: [HACS](https://hacs.xyz/)
|
||||||
|
|
||||||
|
HACS > Overflow Menu > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category: Integration > ADD
|
||||||
|
|
||||||
|
> Xiaomi Home has not been added to the HACS store as a default yet. It's coming soon.
|
||||||
|
|
||||||
|
### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)
|
||||||
|
|
||||||
|
Download and copy `custom_components/xiaomi_home` folder to `config/custom_components` folder in your Home Assistant.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
|
||||||
|
|
||||||
|
### Add MIoT Devices
|
||||||
|
|
||||||
|
After logging in successfully, a dialog box named "Select Home and Devices" pops up. You can select the home containing the device that you want to import in Home Assistant.
|
||||||
|
|
||||||
|
### Multiple User Login
|
||||||
|
|
||||||
|
After a Xiaomi account login and its user configuration are completed, you can continue to add other Xiaomi accounts in the configured Xiaomi Home Integration page.
|
||||||
|
|
||||||
|
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||||
|
|
||||||
|
### Update Configurations
|
||||||
|
|
||||||
|
You can change the configurations in the "Configuration Options" dialog box, in which you can update your user nickname and the list of the devices importing from Xiaomi Home APP, etc.
|
||||||
|
|
||||||
|
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Select the option to update
|
||||||
|
|
||||||
|
### Debug Mode for Action
|
||||||
|
|
||||||
|
You can manually send Action command message with parameters to the device when the debug mode for action is activated. The user interface for sending the Action command with parameters is shown as a Text entity.
|
||||||
|
|
||||||
|
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Debug mode for action
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi officially. You need to use your Xiaomi account to login to get your device list. Xiaomi Home Integration implements OAuth 2.0 login process, which does not keep your account password in the Home Assistant application. However, due to the limitation of the Home Assistant platform, the user information (including device information, certificates, tokens, etc.) of your Xiaomi account will be saved in the Home Assistant configuration file in clear text after successful login. You need to ensure that your Home Assistant configuration file is properly stored. The exposure of your configuration file may result in others logging in with your identity.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
- Does Xiaomi Home Integration support all Xiaomi Home devices?
|
||||||
|
|
||||||
|
Xiaomi Home Integration currently supports most categories of Home device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.
|
||||||
|
|
||||||
|
- Does Xiaomi Home Integration support multiple Xiaomi accounts?
|
||||||
|
|
||||||
|
Yes, it supports multiple Xiaomi accounts. Futhermore, Xiaomi Home Integration allows that devices belonging to different accounts can be added to a same area.
|
||||||
|
|
||||||
|
- Does Xiaomi Home Integration support local control?
|
||||||
|
|
||||||
|
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.4.0_0000 above) or Xiaomi home devices with built-in central hub gateway (software version 0.8.0 above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
|
||||||
|
|
||||||
|
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
|
||||||
|
|
||||||
|
Xiaomi Home Integration can also implement partial local control by enabling Xiaomi LAN control function. Xiaomi LAN control function can only control IP devices (devices connected to the router via WiFi or ethernet cable) in the same local area network as Home Assistant. It cannot control BLE Mesh, ZigBee, etc. devices. This function may cause some abnormalities. We recommend not to use this function. Xiaomi LAN control function is enabled by [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update LAN control configuration
|
||||||
|
|
||||||
|
Xiaomi LAN control function is not restricted by region. It is available in all regions. However, if there is a central gateway in the local area network where Home Assistant is located, even Xiaomi LAN control function is enabled in the integration, it will not take effect.
|
||||||
|
|
||||||
|
- In which regions is Xiaomi Home Integration available?
|
||||||
|
|
||||||
|
Xiaomi Home Integration can be used in the mainland of China, Europe, India, Russia, Singapore, and USA. As user data in Xiaomi Cloud of different regions is isolated, you need to choose your region when importing MIoT devices in the configuration process. Xiaomi Home Integration allows you to import devices of different regions to a same area.
|
||||||
|
|
||||||
|
## Principle of Messaging
|
||||||
|
|
||||||
|
### Control through the Cloud
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="./doc/images/cloud_control.jpg" width=300>
|
||||||
|
|
||||||
|
Image 1: Cloud control architecture
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Xiaomi Home Integration subscribes to the interested device messages on the MQTT Broker in MIoT Cloud. When a device property changes or a device event occurs, the device sends an upstream message to MIoT Cloud, and the MQTT Broker pushes the subscribed device message to Xiaomi Home Integration. Because Xiaomi Home Integration does not need to poll to obtain the current device property value in the cloud, it can immediately receive the notification message when the properties change or the events occur. Thanks to the message subscription mechanism, Xiaomi Home Integration only queries the properties of all devices from the cloud once when the integration configuration is completed, which puts little access pressure on the cloud.
|
||||||
|
|
||||||
|
Xiaomi Home Integration sends command messages to the devices via the HTTP interface of MIoT Cloud to control devices. The device reacts and responds after receiving the downstream message sent forward by MIoT Cloud.
|
||||||
|
|
||||||
|
### Control locally
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="./doc/images/local_control.jpg" width=300>
|
||||||
|
|
||||||
|
Image 2: Local control architecture
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Xiaomi central hub gateway contains a standard MQTT Broker, which implements a complete subscribe-publish mechanism. Xiaomi Home Integration subscribes to the interested device messages through Xiaomi central hub gateway. When a device property changes or a device event occurs, the device sends an upstream message to Xiaomi central hub gateway, and the MQTT Broker pushes the subscribed device message to Xiaomi Home Integration.
|
||||||
|
|
||||||
|
When Xiaomi Home Integration needs to control a device, it publishes a device command message to the MQTT Broker, which is then forwarded to the device by Xiaomi central hub gateway. The device reacts and responds after receiving the downstream message from the gateway.
|
||||||
|
|
||||||
|
## Mapping Relationship between MIoT-Spec-V2 and Home Assistant Entity
|
||||||
|
|
||||||
|
[MIoT-Spec-V2](https://iot.mi.com/v2/new/doc/introduction/knowledge/spec) is the abbreviation for MIoT Specification Version 2, which is an IoT protocol formulated by Xiaomi IoT platform to give a standard functional description of IoT devices. It includes function definition (referred to as data model by other IoT platforms), interaction model, message format, and encoding.
|
||||||
|
|
||||||
|
In MIoT-Spec-V2 protocol, a product is defined as a device. A device contains several services. A service may have some properties, events and actions. Xiaomi Home Integration creates Home Assistant entities according to MIoT-Spec-V2. The conversion relationship is as follows.
|
||||||
|
|
||||||
|
### General Conversion
|
||||||
|
|
||||||
|
- Property
|
||||||
|
|
||||||
|
| format | access | value-list | value-range | Entity in Home Assistant |
|
||||||
|
| ------------ | --------------------- | ------------ | ----------- | ------------------------ |
|
||||||
|
| writable | string | - | - | Text |
|
||||||
|
| writable | bool | - | - | Switch |
|
||||||
|
| writable | not string & not bool | existent | - | Select |
|
||||||
|
| writable | not string & not bool | non-existent | existent | Number |
|
||||||
|
| not writable | - | - | - | Sensor |
|
||||||
|
|
||||||
|
- Event
|
||||||
|
|
||||||
|
MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.
|
||||||
|
|
||||||
|
- Action
|
||||||
|
|
||||||
|
| in | Entity in Home Assistant |
|
||||||
|
| --------- | ------------------------ |
|
||||||
|
| empty | Button |
|
||||||
|
| not empty | Notify |
|
||||||
|
|
||||||
|
If the debug mode for action is activated, the Text entity will be created when the "in" field in the action spec is not empty.
|
||||||
|
|
||||||
|
The "Attribute" item in the entity details page displays the format of the input parameter which is an ordered list, enclosed in square brackets []. The string elements in the list are enclosed in double quotation marks "".
|
||||||
|
|
||||||
|
For example, the "Attributes" item in the details page of the Notify entity converted by the "Intelligent Speaker Execute Text Directive" action of xiaomi.wifispeaker.s12 siid=5, aiid=5 instance shows the action params as `[Text Content(str), Silent Execution(bool)]`. A properly formatted input is `["Hello", true]`.
|
||||||
|
|
||||||
|
### Specific Conversion
|
||||||
|
|
||||||
|
MIoT-Spec-V2 uses URN for defining types. The format is `urn:<namespace>:<type>:<name>:<value>[:<vendor-product>:<version>]`, in which `name` is a human-readable word or phrase describing the instance of device, service, property, event and action. Xiaomi Home Integration first determines whether to convert the MIoT-Spec-V2 instance into a specific Home Assistant entity based on the instance's name. For the instance that does not meet the specific conversion rules, general conversion rules are used for conversion.
|
||||||
|
|
||||||
|
`namespace` is the namespace of MIoT-Spec-V2 instance. When its value is miot-spec-v2, it means that the specification is defined by Xiaomi. When its value is bluetooth-spec, it means that the specification is defined by Bluetooth Special Interest Group (SIG). When its value is not miot-spec-v2 or bluetooth-spec, it means that the specification is defined by other vendors. If MIoT-Spec-V2 `namespace` is not miot-spec-v2, a star mark `*` is added in front of the entity's name .
|
||||||
|
|
||||||
|
- Device
|
||||||
|
|
||||||
|
The conversion follows `SPEC_DEVICE_TRANS_MAP`.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'<device instance name>':{
|
||||||
|
'required':{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The "required" field under "device instance name" indicates the required services of the device. The "optional" field under "device instance name" indicates the optional services of the device. The "entity" field indicates the Home Assistant entity to be created. The "required" and the "optional" field under "service instance name" are required and optional properties, events and actions of the service respectively. The value of "property instance name" under "required" "properties" field is the access mode of the property. The condition for a successful match is that the value of "property instance name" is a subset of the access mode of the corresponding MIoT-Spec-V2 property instance.
|
||||||
|
|
||||||
|
Home Assistant entity will not be created if MIoT-Spec-V2 device instance does not contain all required services, properties, events or actions.
|
||||||
|
|
||||||
|
- Service
|
||||||
|
|
||||||
|
The conversion follows `SPEC_SERVICE_TRANS_MAP`.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The "required" field under "service instance name" indicates the required properties, events and actions of the service. The "optional" field indicates the optional properties, events and actions of the service. The "entity" field indicates the Home Assistant entity to be created. The value of "property instance name" under "required" "properties" field is the access mode of the property. The condition for a successful match is that the value of "property instance name" is a subset of the access mode of the corresponding MIoT-Spec-V2 property instance.
|
||||||
|
|
||||||
|
Home Assistant entity will not be created if MIoT-Spec-V2 service instance does not contain all required properties, events or actions.
|
||||||
|
|
||||||
|
- Property
|
||||||
|
|
||||||
|
The conversion follows `SPEC_PROP_TRANS_MAP`.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'entities':{
|
||||||
|
'<entity name>':{
|
||||||
|
'format': set<str>,
|
||||||
|
'access': set<str>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>':{
|
||||||
|
'device_class': str,
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The "format" field under "entity name" represents the data format of the property, and matching with one value indicates a successful match. The "access" field under "entity name" represents the access mode of the property, and matching with all values is considered a successful match.
|
||||||
|
|
||||||
|
The "entity" field under "property instance name", of which value is one of entity name under "entities" field, indicates the Home Assistant entity to be created. The "device_class" field under "property instance name" indicates the Home Assistant entity's `_attr_device_class`.
|
||||||
|
|
||||||
|
- Event
|
||||||
|
|
||||||
|
The conversion follows `SPEC_EVENT_TRANS_MAP`.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'<event instance name>': str
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of the event instance name indicates `_attr_device_class` of the Home Assistant entity to be created.
|
||||||
|
|
||||||
|
### MIoT-Spec-V2 Filter
|
||||||
|
|
||||||
|
`spec_filter.json` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.
|
||||||
|
|
||||||
|
The format of `spec_filter.json` is as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"<MIoT-Spec-V2 device instance>":{
|
||||||
|
"services": list<service_iid: str>,
|
||||||
|
"properties": list<service_iid.property_iid: str>,
|
||||||
|
"events": list<service_iid.event_iid: str>,
|
||||||
|
"actions": list<service_iid.action_iid: str>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The key of `spec_filter.json` dictionary is the urn excluding the "version " field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.json` does not need to specify the version number of MIoT-Spec-V2 device instance.
|
||||||
|
|
||||||
|
The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{
|
||||||
|
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance.
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
||||||
|
"services": ["3"], # Filter out the service whose iid=3.
|
||||||
|
"properties": ["4.*"] # Filter out all properties in the service whose iid=4.
|
||||||
|
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service.
|
||||||
|
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.
|
||||||
|
|
||||||
|
## Multiple Language Support
|
||||||
|
|
||||||
|
There are 8 languages available for selection in the config flow language option of Xiaomi Home, including Simplified Chinese, Traditional Chinese, English, Spanish, Russian, French, German, and Japanese. The config flow page in Simplified Chinese and English has been manually reviewed by the developer. Other languages are translated by machine translation. If you want to modify the words and sentences in the config flow page, you need to modify the json file of the certain language in `custom_components/xiaomi_home/translations/` directory.
|
||||||
|
|
||||||
|
When displaying Home Assistant entity name, Xiaomi Home downloads the multiple language file configured by the device vendor from MIoT Cloud, which contains translations for MIoT-Spec-V2 instances of the device. `multi_lang.json` is a locally maintained multiple language dictionary, which has a higher priority than the multiple language file obtained from the cloud and can be used to supplement or modify the multiple language translation of devices.
|
||||||
|
|
||||||
|
The format of `multi_lang.json` is as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"<MIoT-Spec-V2 device instance>": {
|
||||||
|
"<language code>": {
|
||||||
|
"<instance code>": <translation: str>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The key of `multi_lang.json` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance.
|
||||||
|
|
||||||
|
The language code is zh-Hans, zh-Hant, en, es, ru, fr, de, or ja, corresponding to the 8 selectable languages mentioned above.
|
||||||
|
|
||||||
|
The instance code is the code of the MIoT-Spec-V2 instance, which is in the format of:
|
||||||
|
|
||||||
|
```
|
||||||
|
service:<siid> # service
|
||||||
|
service:<siid>:property:<piid> # property
|
||||||
|
service:<siid>:property:<piid>:valuelist:<value> # the value in value-list of a property
|
||||||
|
service:<siid>:event:<eiid> # event
|
||||||
|
service:<siid>:action:<aiid> # action
|
||||||
|
```
|
||||||
|
|
||||||
|
siid, piid, eiid, aiid and value are all decimal three-digit integers.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
|
||||||
|
"zh-Hant": {
|
||||||
|
"service:002": "養生壺",
|
||||||
|
"service:002:property:001": "工作狀態",
|
||||||
|
"service:002:property:001:valuelist:000": "待機中",
|
||||||
|
"service:002:action:002": "停止烹飪",
|
||||||
|
"service:005:event:001": "烹飪完成"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [License](./LICENSE.md)
|
||||||
|
- Contribution Guidelines: [English](./doc/CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md)
|
||||||
|
- [ChangeLog](./doc/CHANGELOG.md)
|
||||||
|
- Development Documents: https://developers.home-assistant.io/docs/creating_component_index
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
- miot: core code.
|
||||||
|
- miot/miot_client: Adding a login user in the integration needs adding a miot_client instance.
|
||||||
|
- miot/miot_cloud: Contains functions related to the cloud service, including OAuth login process, HTTP interface functions (to get the user information, to send the device control command, etc.)
|
||||||
|
- miot/miot_device: Device entity, including device information, processing logic of property, event and action.
|
||||||
|
- miot/miot_mips: Message bus for subscribing and publishing method.
|
||||||
|
- miot/miot_spec: Parse MIoT-Spec-V2.
|
||||||
|
- miot/miot_lan: Device LAN control, including device discovery, device control, etc.
|
||||||
|
- miot/miot_mdns: Central hub gateway service LAN discovery.
|
||||||
|
- miot/miot_network: Obtain network status and network information.
|
||||||
|
- miot/miot_storage: Used for integrated file storage.
|
||||||
|
- miot/test: Test scripts.
|
||||||
|
- config_flow: Config flow.
|
||||||
@ -0,0 +1,309 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
The Xiaomi Home integration Init File.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components import persistent_notification
|
||||||
|
from homeassistant.helpers import device_registry, entity_registry
|
||||||
|
|
||||||
|
from .miot.miot_storage import (
|
||||||
|
DeviceManufacturer, MIoTStorage, MIoTCert)
|
||||||
|
from .miot.miot_spec import (
|
||||||
|
MIoTSpecInstance, MIoTSpecParser, MIoTSpecService)
|
||||||
|
from .miot.const import (
|
||||||
|
DEFAULT_INTEGRATION_LANGUAGE, DOMAIN, SUPPORTED_PLATFORMS)
|
||||||
|
from .miot.miot_error import MIoTOauthError
|
||||||
|
from .miot.miot_device import MIoTDevice
|
||||||
|
from .miot.miot_client import MIoTClient, get_miot_instance_async
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
# {[entry_id:str]: MIoTClient}, miot client instance
|
||||||
|
hass.data[DOMAIN].setdefault('miot_clients', {})
|
||||||
|
# {[entry_id:str]: list[MIoTDevice]}
|
||||||
|
hass.data[DOMAIN].setdefault('devices', {})
|
||||||
|
# {[entry_id:str]: entities}
|
||||||
|
hass.data[DOMAIN].setdefault('entities', {})
|
||||||
|
for platform in SUPPORTED_PLATFORMS:
|
||||||
|
hass.data[DOMAIN]['entities'][platform] = []
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up an entry."""
|
||||||
|
def ha_persistent_notify(
|
||||||
|
notify_id: str, title: Optional[str] = None,
|
||||||
|
message: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Send messages in Notifications dialog box."""
|
||||||
|
if title:
|
||||||
|
persistent_notification.async_create(
|
||||||
|
hass=hass, message=message,
|
||||||
|
title=title, notification_id=notify_id)
|
||||||
|
else:
|
||||||
|
persistent_notification.async_dismiss(
|
||||||
|
hass=hass, notification_id=notify_id)
|
||||||
|
|
||||||
|
entry_id = config_entry.entry_id
|
||||||
|
entry_data = dict(config_entry.data)
|
||||||
|
|
||||||
|
ha_persistent_notify(
|
||||||
|
notify_id=f'{entry_id}.oauth_error', title=None, message=None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
miot_client: MIoTClient = await get_miot_instance_async(
|
||||||
|
hass=hass, entry_id=entry_id,
|
||||||
|
entry_data=entry_data,
|
||||||
|
persistent_notify=ha_persistent_notify)
|
||||||
|
# Spec parser
|
||||||
|
spec_parser = MIoTSpecParser(
|
||||||
|
lang=entry_data.get(
|
||||||
|
'integration_language', DEFAULT_INTEGRATION_LANGUAGE),
|
||||||
|
storage=miot_client.miot_storage,
|
||||||
|
loop=miot_client.main_loop
|
||||||
|
)
|
||||||
|
await spec_parser.init_async()
|
||||||
|
# Manufacturer
|
||||||
|
manufacturer: DeviceManufacturer = DeviceManufacturer(
|
||||||
|
storage=miot_client.miot_storage,
|
||||||
|
loop=miot_client.main_loop)
|
||||||
|
await manufacturer.init_async()
|
||||||
|
miot_devices: list[MIoTDevice] = []
|
||||||
|
er = entity_registry.async_get(hass=hass)
|
||||||
|
for did, info in miot_client.device_list.items():
|
||||||
|
spec_instance: MIoTSpecInstance = await spec_parser.parse(
|
||||||
|
urn=info['urn'])
|
||||||
|
if spec_instance is None:
|
||||||
|
_LOGGER.error('spec content is None, %s, %s', did, info)
|
||||||
|
continue
|
||||||
|
device: MIoTDevice = MIoTDevice(
|
||||||
|
miot_client=miot_client,
|
||||||
|
device_info={
|
||||||
|
**info, 'manufacturer': manufacturer.get_name(
|
||||||
|
info.get('manufacturer', ''))},
|
||||||
|
spec_instance=spec_instance)
|
||||||
|
miot_devices.append(device)
|
||||||
|
device.spec_transform()
|
||||||
|
# Remove filter entities and non-standard entities
|
||||||
|
for platform in SUPPORTED_PLATFORMS:
|
||||||
|
# ONLY support filter spec service translate entity
|
||||||
|
if platform in device.entity_list:
|
||||||
|
filter_entities = list(filter(
|
||||||
|
lambda entity: (
|
||||||
|
isinstance(entity.spec, MIoTSpecService)
|
||||||
|
and (
|
||||||
|
entity.spec.need_filter
|
||||||
|
or (
|
||||||
|
miot_client.hide_non_standard_entities
|
||||||
|
and entity.spec.proprietary))
|
||||||
|
),
|
||||||
|
device.entity_list[platform]))
|
||||||
|
for entity in filter_entities:
|
||||||
|
device.entity_list[platform].remove(entity)
|
||||||
|
entity_id = device.gen_service_entity_id(
|
||||||
|
ha_domain=platform, siid=entity.spec.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
if platform in device.prop_list:
|
||||||
|
filter_props = list(filter(
|
||||||
|
lambda prop: (
|
||||||
|
prop.need_filter or (
|
||||||
|
miot_client.hide_non_standard_entities
|
||||||
|
and prop.proprietary)),
|
||||||
|
device.prop_list[platform]))
|
||||||
|
for prop in filter_props:
|
||||||
|
device.prop_list[platform].remove(prop)
|
||||||
|
entity_id = device.gen_prop_entity_id(
|
||||||
|
ha_domain=platform, spec_name=prop.name,
|
||||||
|
siid=prop.service.iid, piid=prop.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
if platform in device.event_list:
|
||||||
|
filter_events = list(filter(
|
||||||
|
lambda event: (
|
||||||
|
event.need_filter or (
|
||||||
|
miot_client.hide_non_standard_entities
|
||||||
|
and event.proprietary)),
|
||||||
|
device.event_list[platform]))
|
||||||
|
for event in filter_events:
|
||||||
|
device.event_list[platform].remove(event)
|
||||||
|
entity_id = device.gen_event_entity_id(
|
||||||
|
ha_domain=platform, spec_name=event.name,
|
||||||
|
siid=event.service.iid, eiid=event.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
if platform in device.action_list:
|
||||||
|
filter_actions = list(filter(
|
||||||
|
lambda action: (
|
||||||
|
action.need_filter or (
|
||||||
|
miot_client.hide_non_standard_entities
|
||||||
|
and action.proprietary)),
|
||||||
|
device.action_list[platform]))
|
||||||
|
for action in filter_actions:
|
||||||
|
device.action_list[platform].remove(action)
|
||||||
|
entity_id = device.gen_action_entity_id(
|
||||||
|
ha_domain=platform, spec_name=action.name,
|
||||||
|
siid=action.service.iid, aiid=action.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
# Remove non-standard action debug entity
|
||||||
|
if platform == 'notify':
|
||||||
|
entity_id = device.gen_action_entity_id(
|
||||||
|
ha_domain='text', spec_name=action.name,
|
||||||
|
siid=action.service.iid, aiid=action.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
# Action debug
|
||||||
|
if miot_client.action_debug:
|
||||||
|
if 'notify' in device.action_list:
|
||||||
|
# Add text entity for debug action
|
||||||
|
device.action_list['action_text'] = (
|
||||||
|
device.action_list['notify'])
|
||||||
|
else:
|
||||||
|
# Remove text entity for debug action
|
||||||
|
for action in device.action_list.get('notify', []):
|
||||||
|
entity_id = device.gen_action_entity_id(
|
||||||
|
ha_domain='text', spec_name=action.name,
|
||||||
|
siid=action.service.iid, aiid=action.iid)
|
||||||
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
|
er.async_remove(entity_id=entity_id)
|
||||||
|
|
||||||
|
hass.data[DOMAIN]['devices'][config_entry.entry_id] = miot_devices
|
||||||
|
await hass.config_entries.async_forward_entry_setups(
|
||||||
|
config_entry, SUPPORTED_PLATFORMS)
|
||||||
|
|
||||||
|
# Remove the deleted devices
|
||||||
|
devices_remove = (await miot_client.miot_storage.load_user_config_async(
|
||||||
|
uid=config_entry.data['uid'],
|
||||||
|
cloud_server=config_entry.data['cloud_server'],
|
||||||
|
keys=['devices_remove'])).get('devices_remove', [])
|
||||||
|
if isinstance(devices_remove, list) and devices_remove:
|
||||||
|
dr = device_registry.async_get(hass)
|
||||||
|
for did in devices_remove:
|
||||||
|
device_entry = dr.async_get_device(
|
||||||
|
identifiers={(
|
||||||
|
DOMAIN,
|
||||||
|
MIoTDevice.gen_did_tag(
|
||||||
|
cloud_server=config_entry.data['cloud_server'],
|
||||||
|
did=did))},
|
||||||
|
connections=None)
|
||||||
|
if not device_entry:
|
||||||
|
_LOGGER.error('remove device not found, %s', did)
|
||||||
|
continue
|
||||||
|
dr.async_remove_device(device_id=device_entry.id)
|
||||||
|
_LOGGER.info(
|
||||||
|
'delete device entry, %s, %s', did, device_entry.id)
|
||||||
|
await miot_client.miot_storage.update_user_config_async(
|
||||||
|
uid=config_entry.data['uid'],
|
||||||
|
cloud_server=config_entry.data['cloud_server'],
|
||||||
|
config={'devices_remove': []})
|
||||||
|
|
||||||
|
await spec_parser.deinit_async()
|
||||||
|
await manufacturer.deinit_async()
|
||||||
|
|
||||||
|
except MIoTOauthError as oauth_error:
|
||||||
|
ha_persistent_notify(
|
||||||
|
notify_id=f'{entry_id}.oauth_error',
|
||||||
|
title='Xiaomi Home Oauth Error',
|
||||||
|
message=f'Please re-add.\r\nerror: {oauth_error}'
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload the entry."""
|
||||||
|
entry_id = config_entry.entry_id
|
||||||
|
# Unload the platform
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
|
config_entry, SUPPORTED_PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN]['entities'].pop(entry_id, None)
|
||||||
|
hass.data[DOMAIN]['devices'].pop(entry_id, None)
|
||||||
|
# Remove integration data
|
||||||
|
miot_client: MIoTClient = hass.data[DOMAIN]['miot_clients'].pop(
|
||||||
|
entry_id, None)
|
||||||
|
if miot_client:
|
||||||
|
await miot_client.deinit_async()
|
||||||
|
del miot_client
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Remove the entry."""
|
||||||
|
entry_data = dict(config_entry.data)
|
||||||
|
uid: str = entry_data['uid']
|
||||||
|
cloud_server: str = entry_data['cloud_server']
|
||||||
|
miot_storage: MIoTStorage = hass.data[DOMAIN]['miot_storage']
|
||||||
|
miot_cert: MIoTCert = MIoTCert(
|
||||||
|
storage=miot_storage, uid=uid, cloud_server=cloud_server)
|
||||||
|
|
||||||
|
# Clean device list
|
||||||
|
await miot_storage.remove_async(
|
||||||
|
domain='miot_devices', name=f'{uid}_{cloud_server}', type_=dict)
|
||||||
|
# Clean user configuration
|
||||||
|
await miot_storage.update_user_config_async(
|
||||||
|
uid=uid, cloud_server=cloud_server, config=None)
|
||||||
|
# Clean cert file
|
||||||
|
await miot_cert.remove_user_cert_async()
|
||||||
|
await miot_cert.remove_user_key_async()
|
||||||
|
return True
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Binary sensor entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('binary_sensor', []):
|
||||||
|
new_entities.append(BinarySensor(
|
||||||
|
miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class BinarySensor(MIoTPropertyEntity, BinarySensorEntity):
|
||||||
|
"""Binary sensor entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the BinarySensor."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Set device_class
|
||||||
|
self._attr_device_class = spec.device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""On/Off state. True if the binary sensor is on, False otherwise."""
|
||||||
|
return self._value is True
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Button entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.button import ButtonEntity
|
||||||
|
|
||||||
|
from .miot.miot_device import MIoTActionEntity, MIoTDevice
|
||||||
|
from .miot.miot_spec import MIoTSpecAction
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for action in miot_device.action_list.get('button', []):
|
||||||
|
new_entities.append(Button(miot_device=miot_device, spec=action))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Button(MIoTActionEntity, ButtonEntity):
|
||||||
|
"""Button entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
|
||||||
|
"""Initialize the Button."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Use default device class
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
return await self.action_async()
|
||||||
@ -0,0 +1,470 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Climate entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
SWING_ON,
|
||||||
|
SWING_OFF,
|
||||||
|
SWING_BOTH,
|
||||||
|
SWING_VERTICAL,
|
||||||
|
SWING_HORIZONTAL,
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
HVACMode,
|
||||||
|
ClimateEntity,
|
||||||
|
ClimateEntityFeature
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('climate', []):
|
||||||
|
new_entities.append(
|
||||||
|
AirConditioner(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||||
|
"""Air conditioner entities for Xiaomi Home."""
|
||||||
|
# service: air-conditioner
|
||||||
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_mode: Optional[MIoTSpecProperty]
|
||||||
|
_prop_target_temp: Optional[MIoTSpecProperty]
|
||||||
|
_prop_target_humi: Optional[MIoTSpecProperty]
|
||||||
|
# service: fan-control
|
||||||
|
_prop_fan_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||||
|
_prop_horizontal_swing: Optional[MIoTSpecProperty]
|
||||||
|
_prop_vertical_swing: Optional[MIoTSpecProperty]
|
||||||
|
# service: environment
|
||||||
|
_prop_env_temp: Optional[MIoTSpecProperty]
|
||||||
|
_prop_env_humi: Optional[MIoTSpecProperty]
|
||||||
|
# service: air-condition-outlet-matching
|
||||||
|
_prop_ac_state: Optional[MIoTSpecProperty]
|
||||||
|
_value_ac_state: Optional[dict[str, int]]
|
||||||
|
|
||||||
|
_hvac_mode_map: Optional[dict[int, HVACMode]]
|
||||||
|
_fan_mode_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Climate."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_icon = 'mdi:air-conditioner'
|
||||||
|
self._attr_supported_features = ClimateEntityFeature(0)
|
||||||
|
self._attr_swing_mode = None
|
||||||
|
self._attr_swing_modes = []
|
||||||
|
|
||||||
|
self._prop_on = None
|
||||||
|
self._prop_mode = None
|
||||||
|
self._prop_target_temp = None
|
||||||
|
self._prop_target_humi = None
|
||||||
|
self._prop_fan_on = None
|
||||||
|
self._prop_fan_level = None
|
||||||
|
self._prop_horizontal_swing = None
|
||||||
|
self._prop_vertical_swing = None
|
||||||
|
self._prop_env_temp = None
|
||||||
|
self._prop_env_humi = None
|
||||||
|
self._prop_ac_state = None
|
||||||
|
self._value_ac_state = None
|
||||||
|
self._hvac_mode_map = None
|
||||||
|
self._fan_mode_map = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'on':
|
||||||
|
if prop.service.name == 'air-conditioner':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
ClimateEntityFeature.TURN_ON)
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
ClimateEntityFeature.TURN_OFF)
|
||||||
|
self._prop_on = prop
|
||||||
|
elif prop.service.name == 'fan-control':
|
||||||
|
self._attr_swing_modes.append(SWING_ON)
|
||||||
|
self._prop_fan_on = prop
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'unknown on property, %s', self.entity_id)
|
||||||
|
elif prop.name == 'mode':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid mode value_list, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._hvac_mode_map = {}
|
||||||
|
for item in prop.value_list:
|
||||||
|
if item['name'].lower() in {'off', 'idle'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.OFF
|
||||||
|
elif item['name'].lower() in {'auto'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.AUTO
|
||||||
|
elif item['name'].lower() in {'cool'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.COOL
|
||||||
|
elif item['name'].lower() in {'heat'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.HEAT
|
||||||
|
elif item['name'].lower() in {'dry'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.DRY
|
||||||
|
elif item['name'].lower() in {'fan'}:
|
||||||
|
self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY
|
||||||
|
self._attr_hvac_modes = list(self._hvac_mode_map.values())
|
||||||
|
self._prop_mode = prop
|
||||||
|
elif prop.name == 'target-temperature':
|
||||||
|
if not isinstance(prop.value_range, dict):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid target-temperature value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._attr_min_temp = prop.value_range['min']
|
||||||
|
self._attr_max_temp = prop.value_range['max']
|
||||||
|
self._attr_target_temperature_step = prop.value_range['step']
|
||||||
|
self._attr_temperature_unit = prop.external_unit
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE)
|
||||||
|
self._prop_target_temp = prop
|
||||||
|
elif prop.name == 'target-humidity':
|
||||||
|
if not isinstance(prop.value_range, dict):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid target-humidity value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._attr_min_humidity = prop.value_range['min']
|
||||||
|
self._attr_max_humidity = prop.value_range['max']
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
ClimateEntityFeature.TARGET_HUMIDITY)
|
||||||
|
self._prop_target_humi = prop
|
||||||
|
elif prop.name == 'fan-level':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid fan-level value_list, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._fan_mode_map = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_fan_modes = list(self._fan_mode_map.values())
|
||||||
|
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
|
||||||
|
self._prop_fan_level = prop
|
||||||
|
elif prop.name == 'horizontal-swing':
|
||||||
|
self._attr_swing_modes.append(SWING_HORIZONTAL)
|
||||||
|
self._prop_horizontal_swing = prop
|
||||||
|
elif prop.name == 'vertical-swing':
|
||||||
|
self._attr_swing_modes.append(SWING_VERTICAL)
|
||||||
|
self._prop_vertical_swing = prop
|
||||||
|
elif prop.name == 'temperature':
|
||||||
|
self._prop_env_temp = prop
|
||||||
|
elif prop.name == 'relative-humidity':
|
||||||
|
self._prop_env_humi = prop
|
||||||
|
|
||||||
|
elif prop.name == 'ac-state':
|
||||||
|
self._prop_ac_state = prop
|
||||||
|
self._value_ac_state = {}
|
||||||
|
self.sub_prop_changed(
|
||||||
|
prop=prop, handler=self.__ac_state_changed)
|
||||||
|
|
||||||
|
# hvac modes
|
||||||
|
if HVACMode.OFF not in self._attr_hvac_modes:
|
||||||
|
self._attr_hvac_modes.append(HVACMode.OFF)
|
||||||
|
# swing modes
|
||||||
|
if (
|
||||||
|
SWING_HORIZONTAL in self._attr_swing_modes
|
||||||
|
and SWING_VERTICAL in self._attr_swing_modes
|
||||||
|
):
|
||||||
|
self._attr_swing_modes.append(SWING_BOTH)
|
||||||
|
if self._attr_swing_modes:
|
||||||
|
self._attr_swing_modes.insert(0, SWING_OFF)
|
||||||
|
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
|
||||||
|
|
||||||
|
async def async_turn_on(self) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
|
"""Set new target hvac mode."""
|
||||||
|
if hvac_mode == HVACMode.OFF and self._prop_on:
|
||||||
|
if not await self.set_property_async(
|
||||||
|
prop=self._prop_on, value=False):
|
||||||
|
raise RuntimeError(
|
||||||
|
f'set climate prop.on failed, {hvac_mode}, '
|
||||||
|
f'{self.entity_id}')
|
||||||
|
return
|
||||||
|
mode_value = self.get_map_value(
|
||||||
|
map_=self._hvac_mode_map, description=hvac_mode)
|
||||||
|
if (
|
||||||
|
mode_value is None or
|
||||||
|
not await self.set_property_async(
|
||||||
|
prop=self._prop_mode, value=mode_value)
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}')
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs):
|
||||||
|
"""Set new target temperature."""
|
||||||
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
|
temp = kwargs[ATTR_TEMPERATURE]
|
||||||
|
if temp > self.max_temp:
|
||||||
|
temp = self.max_temp
|
||||||
|
elif temp < self.min_temp:
|
||||||
|
temp = self.min_temp
|
||||||
|
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_target_temp, value=temp)
|
||||||
|
|
||||||
|
async def async_set_humidity(self, humidity):
|
||||||
|
"""Set new target humidity."""
|
||||||
|
if humidity > self.max_humidity:
|
||||||
|
humidity = self.max_humidity
|
||||||
|
elif humidity < self.min_humidity:
|
||||||
|
humidity = self.min_humidity
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_target_humi, value=humidity)
|
||||||
|
|
||||||
|
async def async_set_swing_mode(self, swing_mode):
|
||||||
|
"""Set new target swing operation."""
|
||||||
|
if swing_mode == SWING_BOTH:
|
||||||
|
if await self.set_property_async(
|
||||||
|
prop=self._prop_horizontal_swing, value=True, update=False):
|
||||||
|
self.set_prop_value(self._prop_horizontal_swing, value=True)
|
||||||
|
if await self.set_property_async(
|
||||||
|
prop=self._prop_vertical_swing, value=True, update=False):
|
||||||
|
self.set_prop_value(self._prop_vertical_swing, value=True)
|
||||||
|
elif swing_mode == SWING_HORIZONTAL:
|
||||||
|
if await self.set_property_async(
|
||||||
|
prop=self._prop_horizontal_swing, value=True, update=False):
|
||||||
|
self.set_prop_value(self._prop_horizontal_swing, value=True)
|
||||||
|
elif swing_mode == SWING_VERTICAL:
|
||||||
|
if await self.set_property_async(
|
||||||
|
prop=self._prop_vertical_swing, value=True, update=False):
|
||||||
|
self.set_prop_value(self._prop_vertical_swing, value=True)
|
||||||
|
elif swing_mode == SWING_ON:
|
||||||
|
if await self.set_property_async(
|
||||||
|
prop=self._prop_fan_on, value=True, update=False):
|
||||||
|
self.set_prop_value(self._prop_fan_on, value=True)
|
||||||
|
elif swing_mode == SWING_OFF:
|
||||||
|
if self._prop_fan_on and await self.set_property_async(
|
||||||
|
prop=self._prop_fan_on, value=False, update=False):
|
||||||
|
self.set_prop_value(self._prop_fan_on, value=False)
|
||||||
|
if self._prop_horizontal_swing and await self.set_property_async(
|
||||||
|
prop=self._prop_horizontal_swing, value=False,
|
||||||
|
update=False):
|
||||||
|
self.set_prop_value(self._prop_horizontal_swing, value=False)
|
||||||
|
if self._prop_vertical_swing and await self.set_property_async(
|
||||||
|
prop=self._prop_vertical_swing, value=False, update=False):
|
||||||
|
self.set_prop_value(self._prop_vertical_swing, value=False)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'unknown swing_mode, {swing_mode}, {self.entity_id}')
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_set_fan_mode(self, fan_mode):
|
||||||
|
"""Set new target fan mode."""
|
||||||
|
mode_value = self.get_map_value(
|
||||||
|
map_=self._fan_mode_map, description=fan_mode)
|
||||||
|
if mode_value is None or not await self.set_property_async(
|
||||||
|
prop=self._prop_fan_level, value=mode_value):
|
||||||
|
raise RuntimeError(
|
||||||
|
f'set climate prop.fan_mode failed, {fan_mode}, '
|
||||||
|
f'{self.entity_id}')
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def target_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the target temperature."""
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_target_temp) if self._prop_target_temp else None
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def target_humidity(self) -> Optional[int]:
|
||||||
|
"""Return the target humidity."""
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_target_humi) if self._prop_target_humi else None
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def current_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_env_temp) if self._prop_env_temp else None
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def current_humidity(self) -> Optional[int]:
|
||||||
|
"""Return the current humidity."""
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_env_humi) if self._prop_env_humi else None
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def hvac_mode(self) -> Optional[HVACMode]:
|
||||||
|
"""Return the hvac mode. e.g., heat, cool mode."""
|
||||||
|
if self._prop_on and self.get_prop_value(prop=self._prop_on) is False:
|
||||||
|
return HVACMode.OFF
|
||||||
|
return self.get_map_description(
|
||||||
|
map_=self._hvac_mode_map,
|
||||||
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def fan_mode(self) -> Optional[str]:
|
||||||
|
"""Return the fan mode.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
|
"""
|
||||||
|
return self.get_map_description(
|
||||||
|
map_=self._fan_mode_map,
|
||||||
|
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def swing_mode(self) -> Optional[str]:
|
||||||
|
"""Return the swing mode.
|
||||||
|
|
||||||
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
|
"""
|
||||||
|
horizontal: bool = (
|
||||||
|
self.get_prop_value(prop=self._prop_horizontal_swing)
|
||||||
|
if self._prop_horizontal_swing else None)
|
||||||
|
vertical: bool = (
|
||||||
|
self.get_prop_value(prop=self._prop_vertical_swing)
|
||||||
|
if self._prop_vertical_swing else None)
|
||||||
|
if horizontal and vertical:
|
||||||
|
return SWING_BOTH
|
||||||
|
if horizontal:
|
||||||
|
return SWING_HORIZONTAL
|
||||||
|
if vertical:
|
||||||
|
return SWING_VERTICAL
|
||||||
|
if self._prop_fan_on:
|
||||||
|
if self.get_prop_value(prop=self._prop_fan_on):
|
||||||
|
return SWING_ON
|
||||||
|
else:
|
||||||
|
return SWING_OFF
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __ac_state_changed(self, prop: MIoTSpecProperty, value: any) -> None:
|
||||||
|
del prop
|
||||||
|
if not isinstance(value, str):
|
||||||
|
_LOGGER.error(
|
||||||
|
'ac_status value format error, %s', value)
|
||||||
|
return
|
||||||
|
v_ac_state = {}
|
||||||
|
v_split = value.split('_')
|
||||||
|
for item in v_split:
|
||||||
|
if len(item) < 2:
|
||||||
|
_LOGGER.error('ac_status value error, %s', item)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
v_ac_state[item[0]] = int(item[1:])
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('ac_status value error, %s', item)
|
||||||
|
# P: status. 0: on, 1: off
|
||||||
|
if 'P' in v_ac_state and self._prop_on:
|
||||||
|
self.set_prop_value(prop=self._prop_on,
|
||||||
|
value=v_ac_state['P'] == 0)
|
||||||
|
# M: model. 0: cool, 1: heat, 2: auto, 3: fan, 4: dry
|
||||||
|
if 'M' in v_ac_state and self._prop_mode:
|
||||||
|
mode: Optional[HVACMode] = {
|
||||||
|
0: HVACMode.COOL,
|
||||||
|
1: HVACMode.HEAT,
|
||||||
|
2: HVACMode.AUTO,
|
||||||
|
3: HVACMode.FAN_ONLY,
|
||||||
|
4: HVACMode.DRY
|
||||||
|
}.get(v_ac_state['M'], None)
|
||||||
|
if mode:
|
||||||
|
self.set_prop_value(
|
||||||
|
prop=self._prop_mode, value=self.get_map_value(
|
||||||
|
map_=self._hvac_mode_map, description=mode))
|
||||||
|
# T: target temperature
|
||||||
|
if 'T' in v_ac_state and self._prop_target_temp:
|
||||||
|
self.set_prop_value(prop=self._prop_target_temp,
|
||||||
|
value=v_ac_state['T'])
|
||||||
|
# S: fan level. 0: auto, 1: low, 2: media, 3: high
|
||||||
|
if 'S' in v_ac_state and self._prop_fan_level:
|
||||||
|
self.set_prop_value(prop=self._prop_fan_level,
|
||||||
|
value=v_ac_state['S'])
|
||||||
|
# D: swing mode. 0: on, 1: off
|
||||||
|
if 'D' in v_ac_state and len(self._attr_swing_modes) == 2:
|
||||||
|
if (
|
||||||
|
SWING_HORIZONTAL in self._attr_swing_modes
|
||||||
|
and self._prop_horizontal_swing
|
||||||
|
):
|
||||||
|
self.set_prop_value(
|
||||||
|
prop=self._prop_horizontal_swing,
|
||||||
|
value=v_ac_state['D'] == 0)
|
||||||
|
elif (
|
||||||
|
SWING_VERTICAL in self._attr_swing_modes
|
||||||
|
and self._prop_vertical_swing
|
||||||
|
):
|
||||||
|
self.set_prop_value(
|
||||||
|
prop=self._prop_vertical_swing,
|
||||||
|
value=v_ac_state['D'] == 0)
|
||||||
|
|
||||||
|
self._value_ac_state.update(v_ac_state)
|
||||||
|
_LOGGER.debug(
|
||||||
|
'ac_state update, %s', self._value_ac_state)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,239 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Cover entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.cover import (
|
||||||
|
ATTR_POSITION,
|
||||||
|
CoverEntity,
|
||||||
|
CoverEntityFeature,
|
||||||
|
CoverDeviceClass
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('cover', []):
|
||||||
|
if data.spec.name == 'curtain':
|
||||||
|
data.spec.device_class = CoverDeviceClass.CURTAIN
|
||||||
|
elif data.spec.name == 'window-opener':
|
||||||
|
data.spec.device_class = CoverDeviceClass.WINDOW
|
||||||
|
new_entities.append(
|
||||||
|
Cover(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Cover(MIoTServiceEntity, CoverEntity):
|
||||||
|
"""Cover entities for Xiaomi Home."""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
_prop_motor_control: Optional[MIoTSpecProperty]
|
||||||
|
_prop_motor_value_open: Optional[int]
|
||||||
|
_prop_motor_value_close: Optional[int]
|
||||||
|
_prop_motor_value_pause: Optional[int]
|
||||||
|
_prop_status: Optional[MIoTSpecProperty]
|
||||||
|
_prop_status_opening: Optional[bool]
|
||||||
|
_prop_status_closing: Optional[bool]
|
||||||
|
_prop_status_stop: Optional[bool]
|
||||||
|
_prop_current_position: Optional[MIoTSpecProperty]
|
||||||
|
_prop_target_position: Optional[MIoTSpecProperty]
|
||||||
|
_prop_position_value_min: Optional[int]
|
||||||
|
_prop_position_value_max: Optional[int]
|
||||||
|
_prop_position_value_range: Optional[int]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Cover."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_device_class = entity_data.spec.device_class
|
||||||
|
self._attr_supported_color_modes = set()
|
||||||
|
self._attr_supported_features = CoverEntityFeature(0)
|
||||||
|
|
||||||
|
self._prop_motor_control = None
|
||||||
|
self._prop_motor_value_open = None
|
||||||
|
self._prop_motor_value_close = None
|
||||||
|
self._prop_motor_value_pause = None
|
||||||
|
self._prop_status = None
|
||||||
|
self._prop_current_position = None
|
||||||
|
self._prop_target_position = None
|
||||||
|
self._prop_position_value_min = None
|
||||||
|
self._prop_position_value_max = None
|
||||||
|
self._prop_position_value_range = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'motor-control':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'motor-control value_list is None, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
for item in prop.value_list:
|
||||||
|
if item['name'].lower() in ['open']:
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
CoverEntityFeature.OPEN)
|
||||||
|
self._prop_motor_value_open = item['value']
|
||||||
|
elif item['name'].lower() in ['close']:
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
CoverEntityFeature.CLOSE)
|
||||||
|
self._prop_motor_value_close = item['value']
|
||||||
|
elif item['name'].lower() in ['pause']:
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
CoverEntityFeature.STOP)
|
||||||
|
self._prop_motor_value_pause = item['value']
|
||||||
|
self._prop_motor_control = prop
|
||||||
|
elif prop.name == 'status':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'status value_list is None, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
for item in prop.value_list:
|
||||||
|
if item['name'].lower() in ['opening']:
|
||||||
|
self._prop_status_opening = item['value']
|
||||||
|
elif item['name'].lower() in ['closing']:
|
||||||
|
self._prop_status_closing = item['value']
|
||||||
|
elif item['name'].lower() in ['stop']:
|
||||||
|
self._prop_status_stop = item['value']
|
||||||
|
self._prop_status = prop
|
||||||
|
elif prop.name == 'current-position':
|
||||||
|
self._prop_current_position = prop
|
||||||
|
elif prop.name == 'target-position':
|
||||||
|
if not isinstance(prop.value_range, dict):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid target-position value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._prop_position_value_min = prop.value_range['min']
|
||||||
|
self._prop_position_value_max = prop.value_range['max']
|
||||||
|
self._prop_position_value_range = (
|
||||||
|
self._prop_position_value_max -
|
||||||
|
self._prop_position_value_min)
|
||||||
|
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
|
||||||
|
self._prop_target_position = prop
|
||||||
|
|
||||||
|
async def async_open_cover(self, **kwargs) -> None:
|
||||||
|
"""Open the cover."""
|
||||||
|
await self.set_property_async(
|
||||||
|
self._prop_motor_control, self._prop_motor_value_open)
|
||||||
|
|
||||||
|
async def async_close_cover(self, **kwargs) -> None:
|
||||||
|
"""Close the cover."""
|
||||||
|
await self.set_property_async(
|
||||||
|
self._prop_motor_control, self._prop_motor_value_close)
|
||||||
|
|
||||||
|
async def async_stop_cover(self, **kwargs) -> None:
|
||||||
|
"""Stop the cover."""
|
||||||
|
await self.set_property_async(
|
||||||
|
self._prop_motor_control, self._prop_motor_value_pause)
|
||||||
|
|
||||||
|
async def async_set_cover_position(self, **kwargs) -> None:
|
||||||
|
"""Set the position of the cover."""
|
||||||
|
pos = kwargs.get(ATTR_POSITION, None)
|
||||||
|
if pos is None:
|
||||||
|
return None
|
||||||
|
pos = round(pos*self._prop_position_value_range/100)
|
||||||
|
return await self.set_property_async(
|
||||||
|
prop=self._prop_target_position, value=pos)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_cover_position(self) -> Optional[int]:
|
||||||
|
"""Return the current position.
|
||||||
|
|
||||||
|
0: the cover is closed, 100: the cover is fully opened, None: unknown.
|
||||||
|
"""
|
||||||
|
pos = self.get_prop_value(prop=self._prop_current_position)
|
||||||
|
if pos is None:
|
||||||
|
return None
|
||||||
|
return round(pos*100/self._prop_position_value_range)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_opening(self) -> Optional[bool]:
|
||||||
|
"""Return if the cover is opening."""
|
||||||
|
if self._prop_status is None:
|
||||||
|
return None
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_status) == self._prop_status_opening
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closing(self) -> Optional[bool]:
|
||||||
|
"""Return if the cover is closing."""
|
||||||
|
if self._prop_status is None:
|
||||||
|
return None
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_status) == self._prop_status_closing
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self) -> Optional[bool]:
|
||||||
|
"""Return if the cover is closed."""
|
||||||
|
return self.get_prop_value(prop=self._prop_current_position) == 0
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Event entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.event import EventEntity
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecEvent
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEventEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for event in miot_device.event_list.get('event', []):
|
||||||
|
new_entities.append(Event(miot_device=miot_device, spec=event))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(MIoTEventEntity, EventEntity):
|
||||||
|
"""Event entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:
|
||||||
|
"""Initialize the Event."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Set device_class
|
||||||
|
self._attr_device_class = spec.device_class
|
||||||
|
|
||||||
|
def on_event_occurred(self, name: str, arguments: list[dict[int, any]]):
|
||||||
|
"""An event is occurred."""
|
||||||
|
self._trigger_event(event_type=name, event_attributes=arguments)
|
||||||
@ -0,0 +1,264 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Fan entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
|
from homeassistant.util.percentage import (
|
||||||
|
percentage_to_ranged_value,
|
||||||
|
ranged_value_to_percentage
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('fan', []):
|
||||||
|
new_entities.append(Fan(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Fan(MIoTServiceEntity, FanEntity):
|
||||||
|
"""Fan entities for Xiaomi Home."""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||||
|
_prop_mode: Optional[MIoTSpecProperty]
|
||||||
|
_prop_horizontal_swing: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
_speed_min: Optional[int]
|
||||||
|
_speed_max: Optional[int]
|
||||||
|
_speed_step: Optional[int]
|
||||||
|
_mode_list: Optional[dict[any, any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Fan."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_preset_modes = []
|
||||||
|
self._attr_supported_features = FanEntityFeature(0)
|
||||||
|
|
||||||
|
self._prop_on = None
|
||||||
|
self._prop_fan_level = None
|
||||||
|
self._prop_mode = None
|
||||||
|
self._prop_horizontal_swing = None
|
||||||
|
self._speed_min = 65535
|
||||||
|
self._speed_max = 0
|
||||||
|
self._speed_step = 1
|
||||||
|
self._mode_list = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'on':
|
||||||
|
self._attr_supported_features |= FanEntityFeature.TURN_ON
|
||||||
|
self._attr_supported_features |= FanEntityFeature.TURN_OFF
|
||||||
|
self._prop_on = prop
|
||||||
|
elif prop.name == 'fan-level':
|
||||||
|
if isinstance(prop.value_range, dict):
|
||||||
|
# Fan level with value-range
|
||||||
|
self._speed_min = prop.value_range['min']
|
||||||
|
self._speed_max = prop.value_range['max']
|
||||||
|
self._speed_step = prop.value_range['step']
|
||||||
|
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||||
|
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||||
|
self._prop_fan_level = prop
|
||||||
|
elif (
|
||||||
|
self._prop_fan_level is None
|
||||||
|
and isinstance(prop.value_list, list)
|
||||||
|
and prop.value_list
|
||||||
|
):
|
||||||
|
# Fan level with value-list
|
||||||
|
for item in prop.value_list:
|
||||||
|
self._speed_min = min(self._speed_min, item['value'])
|
||||||
|
self._speed_max = max(self._speed_max, item['value'])
|
||||||
|
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||||
|
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||||
|
self._prop_fan_level = prop
|
||||||
|
elif prop.name == 'mode':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'mode value_list is None, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._mode_list = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_preset_modes = list(self._mode_list.values())
|
||||||
|
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
|
||||||
|
self._prop_mode = prop
|
||||||
|
elif prop.name == 'horizontal-swing':
|
||||||
|
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||||
|
self._prop_horizontal_swing = prop
|
||||||
|
|
||||||
|
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
return self._mode_list.get(key, None)
|
||||||
|
|
||||||
|
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
for key, value in self._mode_list.items():
|
||||||
|
if value == description:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self, percentage: int = None, preset_mode: str = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Turn the fan on.
|
||||||
|
|
||||||
|
Shall set the percentage or the preset_mode attr to complying
|
||||||
|
if applicable.
|
||||||
|
"""
|
||||||
|
# on
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
|
# percentage
|
||||||
|
if percentage:
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_fan_level,
|
||||||
|
value=int(percentage*self._attr_speed_count/100))
|
||||||
|
# preset_mode
|
||||||
|
if preset_mode:
|
||||||
|
await self.set_property_async(
|
||||||
|
self._prop_mode,
|
||||||
|
value=self.__get_mode_value(description=preset_mode))
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the fan off."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
|
async def async_toggle(self, **kwargs: Any) -> None:
|
||||||
|
"""Toggle the fan."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=not self.is_on)
|
||||||
|
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the percentage of the fan speed."""
|
||||||
|
if percentage > 0:
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_fan_level,
|
||||||
|
value=int(percentage_to_ranged_value(
|
||||||
|
low_high_range=(self._speed_min, self._speed_max),
|
||||||
|
percentage=percentage)))
|
||||||
|
if not self.is_on:
|
||||||
|
# If the fan is off, turn it on.
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
|
else:
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set the preset mode."""
|
||||||
|
await self.set_property_async(
|
||||||
|
self._prop_mode,
|
||||||
|
value=self.__get_mode_value(description=preset_mode))
|
||||||
|
|
||||||
|
async def async_set_direction(self, direction: str) -> None:
|
||||||
|
"""Set the direction of the fan."""
|
||||||
|
|
||||||
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
|
"""Oscillate the fan."""
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_horizontal_swing, value=oscillating)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> Optional[bool]:
|
||||||
|
"""Return if the fan is on. """
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_on) if self._prop_on else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_mode(self) -> Optional[str]:
|
||||||
|
"""Return the current preset mode,
|
||||||
|
e.g., auto, smart, eco, favorite."""
|
||||||
|
return (
|
||||||
|
self.__get_mode_description(
|
||||||
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
|
if self._prop_mode else None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage(self) -> Optional[int]:
|
||||||
|
"""Return the current percentage of the fan speed."""
|
||||||
|
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
||||||
|
return ranged_value_to_percentage(
|
||||||
|
low_high_range=(self._speed_min, self._speed_max),
|
||||||
|
value=fan_level) if fan_level else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oscillating(self) -> Optional[bool]:
|
||||||
|
"""Return if the fan is oscillating."""
|
||||||
|
return (
|
||||||
|
self.get_prop_value(
|
||||||
|
prop=self._prop_horizontal_swing)
|
||||||
|
if self._prop_horizontal_swing else None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage_step(self) -> float:
|
||||||
|
"""Return the step of the fan speed."""
|
||||||
|
return self._speed_step
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Humidifier entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.humidifier import (
|
||||||
|
HumidifierEntity,
|
||||||
|
HumidifierDeviceClass,
|
||||||
|
HumidifierEntityFeature
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('humidifier', []):
|
||||||
|
data.device_class = HumidifierDeviceClass.HUMIDIFIER
|
||||||
|
new_entities.append(
|
||||||
|
Humidifier(miot_device=miot_device, entity_data=data))
|
||||||
|
for data in miot_device.entity_list.get('dehumidifier', []):
|
||||||
|
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
|
||||||
|
new_entities.append(Humidifier(
|
||||||
|
miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||||
|
"""Humidifier entities for Xiaomi Home."""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_mode: Optional[MIoTSpecProperty]
|
||||||
|
_prop_target_humidity: Optional[MIoTSpecProperty]
|
||||||
|
_prop_humidity: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
_mode_list: dict[any, any]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Humidifier."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_device_class = entity_data.device_class
|
||||||
|
self._attr_supported_features = HumidifierEntityFeature(0)
|
||||||
|
self._prop_on = None
|
||||||
|
self._prop_mode = None
|
||||||
|
self._prop_target_humidity = None
|
||||||
|
self._prop_humidity = None
|
||||||
|
self._mode_list = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
# on
|
||||||
|
if prop.name == 'on':
|
||||||
|
self._prop_on = prop
|
||||||
|
# target-humidity
|
||||||
|
elif prop.name == 'target-humidity':
|
||||||
|
if not isinstance(prop.value_range, dict):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid target-humidity value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._attr_min_humidity = prop.value_range['min']
|
||||||
|
self._attr_max_humidity = prop.value_range['max']
|
||||||
|
self._prop_target_humidity = prop
|
||||||
|
# mode
|
||||||
|
elif prop.name == 'mode':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'mode value_list is None, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._mode_list = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_available_modes = list(
|
||||||
|
self._mode_list.values())
|
||||||
|
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||||
|
self._prop_mode = prop
|
||||||
|
# relative-humidity
|
||||||
|
elif prop.name == 'relative-humidity':
|
||||||
|
self._prop_humidity = prop
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the humidifier on."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the humidifier off."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
|
async def async_set_humidity(self, humidity: int) -> None:
|
||||||
|
"""Set new target humidity."""
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_target_humidity, value=humidity)
|
||||||
|
|
||||||
|
async def async_set_mode(self, mode: str) -> None:
|
||||||
|
"""Set new target preset mode."""
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_mode, value=self.__get_mode_value(description=mode))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> Optional[bool]:
|
||||||
|
"""Return if the humidifier is on."""
|
||||||
|
return self.get_prop_value(prop=self._prop_on)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_humidity(self) -> Optional[int]:
|
||||||
|
"""Return the current humidity."""
|
||||||
|
return self.get_prop_value(prop=self._prop_humidity)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_humidity(self) -> Optional[int]:
|
||||||
|
"""Return the target humidity."""
|
||||||
|
return self.get_prop_value(prop=self._prop_target_humidity)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self) -> Optional[str]:
|
||||||
|
"""Return the current preset mode."""
|
||||||
|
return self.__get_mode_description(
|
||||||
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
|
|
||||||
|
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||||
|
"""Convert mode value to description."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
return self._mode_list.get(key, None)
|
||||||
|
|
||||||
|
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||||
|
"""Convert mode description to value."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
for key, value in self._mode_list.items():
|
||||||
|
if value == description:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
@ -0,0 +1,300 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Light entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
|
ATTR_RGB_COLOR,
|
||||||
|
ATTR_HS_COLOR,
|
||||||
|
ATTR_EFFECT,
|
||||||
|
LightEntity,
|
||||||
|
LightEntityFeature,
|
||||||
|
ColorMode
|
||||||
|
)
|
||||||
|
from homeassistant.util.color import (
|
||||||
|
value_to_brightness,
|
||||||
|
brightness_to_value,
|
||||||
|
color_hs_to_RGB
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('light', []):
|
||||||
|
new_entities.append(
|
||||||
|
Light(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Light(MIoTServiceEntity, LightEntity):
|
||||||
|
"""Light entities for Xiaomi Home."""
|
||||||
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_brightness: Optional[MIoTSpecProperty]
|
||||||
|
_prop_color_temp: Optional[MIoTSpecProperty]
|
||||||
|
_prop_color: Optional[MIoTSpecProperty]
|
||||||
|
_prop_mode: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
_brightness_scale: Optional[tuple[int, int]]
|
||||||
|
_mode_list: Optional[dict[any, any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Light."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_color_mode = None
|
||||||
|
self._attr_supported_color_modes = set()
|
||||||
|
self._attr_supported_features = LightEntityFeature(0)
|
||||||
|
if miot_device.did.startswith('group.'):
|
||||||
|
self._attr_icon = 'mdi:lightbulb-group'
|
||||||
|
|
||||||
|
self._prop_on = None
|
||||||
|
self._prop_brightness = None
|
||||||
|
self._prop_color_temp = None
|
||||||
|
self._prop_color = None
|
||||||
|
self._prop_mode = None
|
||||||
|
self._brightness_scale = None
|
||||||
|
self._mode_list = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
# on
|
||||||
|
if prop.name == 'on':
|
||||||
|
self._prop_on = prop
|
||||||
|
# brightness
|
||||||
|
if prop.name == 'brightness':
|
||||||
|
if isinstance(prop.value_range, dict):
|
||||||
|
self._brightness_scale = (
|
||||||
|
prop.value_range['min'], prop.value_range['max'])
|
||||||
|
self._prop_brightness = prop
|
||||||
|
elif (
|
||||||
|
self._mode_list is None
|
||||||
|
and isinstance(prop.value_list, list)
|
||||||
|
and prop.value_list
|
||||||
|
):
|
||||||
|
# For value-list brightness
|
||||||
|
self._mode_list = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_effect_list = list(self._mode_list.values())
|
||||||
|
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||||
|
self._prop_mode = prop
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid brightness format, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
# color-temperature
|
||||||
|
if prop.name == 'color-temperature':
|
||||||
|
if not isinstance(prop.value_range, dict):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid color-temperature value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._attr_min_color_temp_kelvin = prop.value_range['min']
|
||||||
|
self._attr_max_color_temp_kelvin = prop.value_range['max']
|
||||||
|
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||||
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
|
self._prop_color_temp = prop
|
||||||
|
# color
|
||||||
|
if prop.name == 'color':
|
||||||
|
self._attr_supported_color_modes.add(ColorMode.RGB)
|
||||||
|
self._attr_color_mode = ColorMode.RGB
|
||||||
|
self._prop_color = prop
|
||||||
|
# mode
|
||||||
|
if prop.name == 'mode':
|
||||||
|
mode_list = None
|
||||||
|
if (
|
||||||
|
isinstance(prop.value_list, list)
|
||||||
|
and prop.value_list
|
||||||
|
):
|
||||||
|
mode_list = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
elif isinstance(prop.value_range, dict):
|
||||||
|
mode_list = {}
|
||||||
|
for value in range(
|
||||||
|
prop.value_range['min'], prop.value_range['max']):
|
||||||
|
mode_list[value] = f'{value}'
|
||||||
|
if mode_list:
|
||||||
|
self._mode_list = mode_list
|
||||||
|
self._attr_effect_list = list(self._mode_list.values())
|
||||||
|
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||||
|
self._prop_mode = prop
|
||||||
|
else:
|
||||||
|
_LOGGER.error('invalid mode format, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._attr_supported_color_modes:
|
||||||
|
if self._prop_brightness:
|
||||||
|
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||||
|
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||||
|
elif self._prop_on:
|
||||||
|
self._attr_supported_color_modes.add(ColorMode.ONOFF)
|
||||||
|
self._attr_color_mode = ColorMode.ONOFF
|
||||||
|
|
||||||
|
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||||
|
"""Convert mode value to description."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
return self._mode_list.get(key, None)
|
||||||
|
|
||||||
|
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||||
|
"""Convert mode description to value."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
for key, value in self._mode_list.items():
|
||||||
|
if value == description:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> Optional[bool]:
|
||||||
|
"""Return if the light is on."""
|
||||||
|
value_on = self.get_prop_value(prop=self._prop_on)
|
||||||
|
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||||
|
if isinstance(value_on, int):
|
||||||
|
value_on = value_on == 1
|
||||||
|
return value_on
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self) -> Optional[int]:
|
||||||
|
"""Return the brightness."""
|
||||||
|
brightness_value = self.get_prop_value(prop=self._prop_brightness)
|
||||||
|
if brightness_value is None:
|
||||||
|
return None
|
||||||
|
return value_to_brightness(self._brightness_scale, brightness_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp_kelvin(self) -> Optional[int]:
|
||||||
|
"""Return the color temperature."""
|
||||||
|
return self.get_prop_value(prop=self._prop_color_temp)
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def rgb_color(self) -> Optional[tuple[int, int, int]]:
|
||||||
|
"""Return the rgb color value."""
|
||||||
|
rgb = self.get_prop_value(prop=self._prop_color)
|
||||||
|
if rgb is None:
|
||||||
|
return None
|
||||||
|
r = (rgb >> 16) & 0xFF
|
||||||
|
g = (rgb >> 8) & 0xFF
|
||||||
|
b = rgb & 0xFF
|
||||||
|
return r, g, b
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def effect(self) -> Optional[str]:
|
||||||
|
"""Return the current mode."""
|
||||||
|
return self.__get_mode_description(
|
||||||
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs) -> None:
|
||||||
|
"""Turn the light on.
|
||||||
|
|
||||||
|
Shall set attributes in kwargs if applicable.
|
||||||
|
"""
|
||||||
|
result: bool = False
|
||||||
|
# on
|
||||||
|
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||||
|
value_on = True if self._prop_on.format_ == 'bool' else 1
|
||||||
|
result = await self.set_property_async(
|
||||||
|
prop=self._prop_on, value=value_on)
|
||||||
|
# brightness
|
||||||
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
brightness = brightness_to_value(
|
||||||
|
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
|
||||||
|
result = await self.set_property_async(
|
||||||
|
prop=self._prop_brightness, value=brightness)
|
||||||
|
# color-temperature
|
||||||
|
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||||
|
result = await self.set_property_async(
|
||||||
|
prop=self._prop_color_temp,
|
||||||
|
value=kwargs[ATTR_COLOR_TEMP_KELVIN])
|
||||||
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
|
# rgb color
|
||||||
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
|
r = kwargs[ATTR_RGB_COLOR][0]
|
||||||
|
g = kwargs[ATTR_RGB_COLOR][1]
|
||||||
|
b = kwargs[ATTR_RGB_COLOR][2]
|
||||||
|
rgb = (r << 16) | (g << 8) | b
|
||||||
|
result = await self.set_property_async(
|
||||||
|
prop=self._prop_color, value=rgb)
|
||||||
|
self._attr_color_mode = ColorMode.RGB
|
||||||
|
# mode
|
||||||
|
if ATTR_EFFECT in kwargs:
|
||||||
|
result = await self.set_property_async(
|
||||||
|
prop=self._prop_mode,
|
||||||
|
value=self.__get_mode_value(description=kwargs[ATTR_EFFECT]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
|
"""Turn the light off."""
|
||||||
|
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||||
|
value_on = False if self._prop_on.format_ == 'bool' else 0
|
||||||
|
return await self.set_property_async(prop=self._prop_on, value=value_on)
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"domain": "xiaomi_home",
|
||||||
|
"name": "Xiaomi Home",
|
||||||
|
"codeowners": [
|
||||||
|
"@XiaoMi"
|
||||||
|
],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [
|
||||||
|
"http",
|
||||||
|
"persistent_notification",
|
||||||
|
"ffmpeg",
|
||||||
|
"zeroconf"
|
||||||
|
],
|
||||||
|
"documentation": "https://github.com/XiaoMi/ha_xiaomi_home/blob/main/README.md",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"issue_tracker": "https://github.com/XiaoMi/ha_xiaomi_home/issues",
|
||||||
|
"loggers": [
|
||||||
|
"Xiaomi Home"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"construct>=2.10.56",
|
||||||
|
"paho-mqtt<=2.0.0",
|
||||||
|
"numpy",
|
||||||
|
"cryptography",
|
||||||
|
"psutil"
|
||||||
|
],
|
||||||
|
"version": "v0.1.0",
|
||||||
|
"zeroconf": [
|
||||||
|
"_miot-central._tcp.local."
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Common utilities.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
import hashlib
|
||||||
|
from paho.mqtt.client import MQTTMatcher
|
||||||
|
|
||||||
|
|
||||||
|
def calc_group_id(uid: str, home_id: str) -> str:
|
||||||
|
"""Calculate the group ID based on a user ID and a home ID."""
|
||||||
|
return hashlib.sha1(
|
||||||
|
f'{uid}central_service{home_id}'.encode('utf-8')).hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_file(json_file: str) -> dict:
|
||||||
|
"""Load a JSON file."""
|
||||||
|
with open(json_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_int(value: int, ratio: float) -> int:
|
||||||
|
"""Randomize an integer value."""
|
||||||
|
return int(value * (1 - ratio + random.random()*2*ratio))
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTMatcher(MQTTMatcher):
|
||||||
|
"""MIoT Pub/Sub topic matcher."""
|
||||||
|
|
||||||
|
def iter_all_nodes(self) -> any:
|
||||||
|
"""Return an iterator on all nodes with their paths and contents."""
|
||||||
|
def rec(node, path):
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
if node._content:
|
||||||
|
yield ('/'.join(path), node._content)
|
||||||
|
for part, child in node._children.items():
|
||||||
|
yield from rec(child, path + [part])
|
||||||
|
return rec(self._root, [])
|
||||||
|
|
||||||
|
def get(self, topic: str) -> Optional[any]:
|
||||||
|
try:
|
||||||
|
return self[topic]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Constants.
|
||||||
|
"""
|
||||||
|
DOMAIN: str = 'xiaomi_home'
|
||||||
|
DEFAULT_NAME: str = 'Xiaomi Home'
|
||||||
|
|
||||||
|
DEFAULT_NICK_NAME: str = 'Xiaomi'
|
||||||
|
|
||||||
|
MIHOME_HTTP_API_TIMEOUT: int = 30
|
||||||
|
MIHOME_MQTT_KEEPALIVE: int = 60
|
||||||
|
# seconds, 3 days
|
||||||
|
MIHOME_CERT_EXPIRE_MARGIN: int = 3600*24*3
|
||||||
|
|
||||||
|
NETWORK_REFRESH_INTERVAL: int = 30
|
||||||
|
|
||||||
|
OAUTH2_CLIENT_ID: str = '2882303761520251711'
|
||||||
|
OAUTH2_AUTH_URL: str = 'https://account.xiaomi.com/oauth2/authorize'
|
||||||
|
DEFAULT_OAUTH2_API_HOST: str = 'ha.api.io.mi.com'
|
||||||
|
|
||||||
|
# seconds, 14 days
|
||||||
|
SPEC_STD_LIB_EFFECTIVE_TIME = 3600*24*14
|
||||||
|
# seconds, 14 days
|
||||||
|
MANUFACTURER_EFFECTIVE_TIME = 3600*24*14
|
||||||
|
|
||||||
|
SUPPORTED_PLATFORMS: list = [
|
||||||
|
# 'alarm_control_panel',
|
||||||
|
'binary_sensor',
|
||||||
|
'button',
|
||||||
|
'climate',
|
||||||
|
# 'camera',
|
||||||
|
# 'conversation',
|
||||||
|
'cover',
|
||||||
|
# 'device_tracker',
|
||||||
|
'event',
|
||||||
|
'fan',
|
||||||
|
'humidifier',
|
||||||
|
'light',
|
||||||
|
# 'lock',
|
||||||
|
# 'media_player',
|
||||||
|
'notify',
|
||||||
|
'number',
|
||||||
|
# 'remote',
|
||||||
|
# 'scene',
|
||||||
|
'select',
|
||||||
|
'sensor',
|
||||||
|
'switch',
|
||||||
|
'text',
|
||||||
|
'vacuum',
|
||||||
|
'water_heater',
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_CLOUD_SERVER: str = 'cn'
|
||||||
|
CLOUD_SERVERS: dict = {
|
||||||
|
'cn': '中国大陆',
|
||||||
|
'de': 'Europe',
|
||||||
|
'i2': 'India',
|
||||||
|
'ru': 'Russia',
|
||||||
|
'sg': 'Singapore',
|
||||||
|
'us': 'United States'
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORT_CENTRAL_GATEWAY_CTRL: list = ['cn']
|
||||||
|
|
||||||
|
DEFAULT_INTEGRATION_LANGUAGE: str = 'en'
|
||||||
|
INTEGRATION_LANGUAGES = {
|
||||||
|
'zh-Hans': '简体中文',
|
||||||
|
'zh-Hant': '繁體中文',
|
||||||
|
'en': 'English',
|
||||||
|
'es': 'Español',
|
||||||
|
'ru': 'Русский',
|
||||||
|
'fr': 'Français',
|
||||||
|
'de': 'Deutsch',
|
||||||
|
'ja': '日本語'
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_CTRL_MODE: str = 'auto'
|
||||||
|
|
||||||
|
# Registered in Xiaomi OAuth 2.0 Service
|
||||||
|
# DO NOT CHANGE UNLESS YOU HAVE AN ADMINISTRATOR PERMISSION
|
||||||
|
OAUTH_REDIRECT_URL: str = 'http://homeassistant.local:8123'
|
||||||
|
|
||||||
|
MIHOME_CA_CERT_STR: str = '-----BEGIN CERTIFICATE-----\n' \
|
||||||
|
'MIIBazCCAQ+gAwIBAgIEA/UKYDAMBggqhkjOPQQDAgUAMCIxEzARBgNVBAoTCk1p\n' \
|
||||||
|
'amlhIFJvb3QxCzAJBgNVBAYTAkNOMCAXDTE2MTEyMzAxMzk0NVoYDzIwNjYxMTEx\n' \
|
||||||
|
'MDEzOTQ1WjAiMRMwEQYDVQQKEwpNaWppYSBSb290MQswCQYDVQQGEwJDTjBZMBMG\n' \
|
||||||
|
'ByqGSM49AgEGCCqGSM49AwEHA0IABL71iwLa4//4VBqgRI+6xE23xpovqPCxtv96\n' \
|
||||||
|
'2VHbZij61/Ag6jmi7oZ/3Xg/3C+whglcwoUEE6KALGJ9vccV9PmjLzAtMAwGA1Ud\n' \
|
||||||
|
'EwQFMAMBAf8wHQYDVR0OBBYEFJa3onw5sblmM6n40QmyAGDI5sURMAwGCCqGSM49\n' \
|
||||||
|
'BAMCBQADSAAwRQIgchciK9h6tZmfrP8Ka6KziQ4Lv3hKfrHtAZXMHPda4IYCIQCG\n' \
|
||||||
|
'az93ggFcbrG9u2wixjx1HKW4DUA5NXZG0wWQTpJTbQ==\n' \
|
||||||
|
'-----END CERTIFICATE-----\n' \
|
||||||
|
'-----BEGIN CERTIFICATE-----\n' \
|
||||||
|
'MIIBjzCCATWgAwIBAgIBATAKBggqhkjOPQQDAjAiMRMwEQYDVQQKEwpNaWppYSBS\n' \
|
||||||
|
'b290MQswCQYDVQQGEwJDTjAgFw0yMjA2MDkxNDE0MThaGA8yMDcyMDUyNzE0MTQx\n' \
|
||||||
|
'OFowLDELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFE1JT1QgQ0VOVFJBTCBHQVRFV0FZ\n' \
|
||||||
|
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdYrzbnp/0x/cZLZnuEDXTFf8mhj4\n' \
|
||||||
|
'CVpZPwgj9e9Ve5r3K7zvu8Jjj7JF1JjQYvEC6yhp1SzBgglnK4L8xQzdiqNQME4w\n' \
|
||||||
|
'HQYDVR0OBBYEFCf9+YBU7pXDs6K6CAQPRhlGJ+cuMB8GA1UdIwQYMBaAFJa3onw5\n' \
|
||||||
|
'sblmM6n40QmyAGDI5sURMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIh\n' \
|
||||||
|
'AKUv+c8v98vypkGMTzMwckGjjVqTef8xodsy6PhcSCq+AiA/n9mDs62hAo5zXyJy\n' \
|
||||||
|
'Bs1s7mqXPf1XgieoxIvs1MqyiA==\n' \
|
||||||
|
'-----END CERTIFICATE-----\n'
|
||||||
|
|
||||||
|
MIHOME_CA_CERT_SHA256: str = \
|
||||||
|
'8b7bf306be3632e08b0ead308249e5f2b2520dc921ad143872d5fcc7c68d6759'
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"other": {
|
||||||
|
"devices": "Geräte",
|
||||||
|
"found_central_gateway": ", lokales zentrales Gateway gefunden"
|
||||||
|
},
|
||||||
|
"control_mode": {
|
||||||
|
"auto": "automatisch",
|
||||||
|
"cloud": "Cloud"
|
||||||
|
},
|
||||||
|
"room_name_rule": {
|
||||||
|
"none": "nicht synchronisieren",
|
||||||
|
"home_room": "Hausname und Raumname (Xiaomi Home Schlafzimmer)",
|
||||||
|
"room": "Raumname (Schlafzimmer)",
|
||||||
|
"home": "Hausname (Xiaomi Home)"
|
||||||
|
},
|
||||||
|
"option_status": {
|
||||||
|
"enable": "aktivieren",
|
||||||
|
"disable": "deaktivieren"
|
||||||
|
},
|
||||||
|
"lan_ctrl_config": {
|
||||||
|
"notice_net_dup": "\r\n**[Hinweis]** Es wurden mehrere Netzwerkkarten erkannt, die möglicherweise mit demselben Netzwerk verbunden sind. Bitte achten Sie auf die Auswahl.",
|
||||||
|
"net_unavailable": "Schnittstelle nicht verfügbar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"miot": {
|
||||||
|
"client": {
|
||||||
|
"invalid_oauth_info": "Ungültige Authentifizierungsinformationen, Cloud-Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen",
|
||||||
|
"invalid_device_cache": "Ungültige Gerätecache-Informationen, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren",
|
||||||
|
"invalid_cert_info": "Ungültiges Benutzerzertifikat, lokale zentrale Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen",
|
||||||
|
"device_cloud_error": "Fehler beim Abrufen von Geräteinformationen aus der Cloud, bitte überprüfen Sie die lokale Netzwerkverbindung",
|
||||||
|
"xiaomi_home_error_title": "Xiaomi Home-Integrationsfehler",
|
||||||
|
"xiaomi_home_error": "Fehler **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Optionen-Seite, um die Konfiguration erneut durchzuführen.\n\n**Fehlermeldung**: \n{message}",
|
||||||
|
"device_list_changed_title": "Xiaomi Home-Geräteliste geändert",
|
||||||
|
"device_list_changed": "Änderung der Geräteinformationen **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Integrations-Optionen-Seite, klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren.\n\nAktueller Netzwerkstatus: {network_status}\n{message}\n",
|
||||||
|
"device_list_add": "\n**{count} neue Geräte:** \n{message}",
|
||||||
|
"device_list_del": "\n**{count} Geräte nicht verfügbar:** \n{message}",
|
||||||
|
"device_list_offline": "\n**{count} Geräte offline:** \n{message}",
|
||||||
|
"network_status_online": "Online",
|
||||||
|
"network_status_offline": "Offline",
|
||||||
|
"device_exec_error": "Fehler bei der Ausführung"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"common": {
|
||||||
|
"-10000": "Unbekannter Fehler",
|
||||||
|
"-10001": "Dienst nicht verfügbar",
|
||||||
|
"-10002": "Ungültiger Parameter",
|
||||||
|
"-10003": "Unzureichende Ressourcen",
|
||||||
|
"-10004": "Interner Fehler",
|
||||||
|
"-10005": "Unzureichende Berechtigungen",
|
||||||
|
"-10006": "Ausführungszeitüberschreitung",
|
||||||
|
"-10007": "Gerät offline oder nicht vorhanden",
|
||||||
|
"-10020": "Nicht autorisiert (OAuth2)",
|
||||||
|
"-10030": "Ungültiges Token (HTTP)",
|
||||||
|
"-10040": "Ungültiges Nachrichtenformat",
|
||||||
|
"-10050": "Ungültiges Zertifikat",
|
||||||
|
"-704000000": "Unbekannter Fehler",
|
||||||
|
"-704010000": "Nicht autorisiert (Gerät wurde möglicherweise gelöscht)",
|
||||||
|
"-704014006": "Gerätebeschreibung nicht gefunden",
|
||||||
|
"-704030013": "Eigenschaft nicht lesbar",
|
||||||
|
"-704030023": "Eigenschaft nicht beschreibbar",
|
||||||
|
"-704030033": "Eigenschaft nicht abonnierbar",
|
||||||
|
"-704040002": "Dienst existiert nicht",
|
||||||
|
"-704040003": "Eigenschaft existiert nicht",
|
||||||
|
"-704040004": "Ereignis existiert nicht",
|
||||||
|
"-704040005": "Aktion existiert nicht",
|
||||||
|
"-704040999": "Funktion nicht online",
|
||||||
|
"-704042001": "Gerät existiert nicht",
|
||||||
|
"-704042011": "Gerät offline",
|
||||||
|
"-704053036": "Gerätebetrieb zeitüberschreitung",
|
||||||
|
"-704053100": "Gerät kann diese Operation im aktuellen Zustand nicht ausführen",
|
||||||
|
"-704083036": "Gerätebetrieb zeitüberschreitung",
|
||||||
|
"-704090001": "Gerät existiert nicht",
|
||||||
|
"-704220008": "Ungültige ID",
|
||||||
|
"-704220025": "Aktionsparameteranzahl stimmt nicht überein",
|
||||||
|
"-704220035": "Aktionsparameterfehler",
|
||||||
|
"-704220043": "Eigenschaftswertfehler",
|
||||||
|
"-704222034": "Aktionsrückgabewertfehler",
|
||||||
|
"-705004000": "Unbekannter Fehler",
|
||||||
|
"-705004501": "Unbekannter Fehler",
|
||||||
|
"-705201013": "Eigenschaft nicht lesbar",
|
||||||
|
"-705201015": "Aktionsausführungsfehler",
|
||||||
|
"-705201023": "Eigenschaft nicht beschreibbar",
|
||||||
|
"-705201033": "Eigenschaft nicht abonnierbar",
|
||||||
|
"-706012000": "Unbekannter Fehler",
|
||||||
|
"-706012013": "Eigenschaft nicht lesbar",
|
||||||
|
"-706012015": "Aktionsausführungsfehler",
|
||||||
|
"-706012023": "Eigenschaft nicht beschreibbar",
|
||||||
|
"-706012033": "Eigenschaft nicht abonnierbar",
|
||||||
|
"-706012043": "Eigenschaftswertfehler",
|
||||||
|
"-706014006": "Gerätebeschreibung nicht gefunden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"other": {
|
||||||
|
"devices": "Devices",
|
||||||
|
"found_central_gateway": ", Found Local Central Hub Gateway"
|
||||||
|
},
|
||||||
|
"control_mode": {
|
||||||
|
"auto": "Auto",
|
||||||
|
"cloud": "Cloud"
|
||||||
|
},
|
||||||
|
"room_name_rule": {
|
||||||
|
"none": "Do not synchronize",
|
||||||
|
"home_room": "Home Name and Room Name (Xiaomi Home Bedroom)",
|
||||||
|
"room": "Room Name (Bedroom)",
|
||||||
|
"home": "Home Name (Xiaomi Home)"
|
||||||
|
},
|
||||||
|
"option_status": {
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable"
|
||||||
|
},
|
||||||
|
"lan_ctrl_config": {
|
||||||
|
"notice_net_dup": "\r\n**[Notice]** Multiple network cards detected that may be connected to the same network. Please pay attention to the selection.",
|
||||||
|
"net_unavailable": "Interface unavailable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"miot": {
|
||||||
|
"client": {
|
||||||
|
"invalid_oauth_info": "Authentication information is invalid, cloud link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate",
|
||||||
|
"invalid_device_cache": "Cache device information is abnormal, please enter the Xiaomi Home integration page, click 'Options->Update device list', update the local cache",
|
||||||
|
"invalid_cert_info": "Invalid user certificate, local central link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate",
|
||||||
|
"device_cloud_error": "An exception occurred when obtaining device information from the cloud, please check the local network connection",
|
||||||
|
"xiaomi_home_error_title": "Xiaomi Home Integration Error",
|
||||||
|
"xiaomi_home_error": "Detected **{nick_name}({uid}, {cloud_server})** error, please enter the options page to reconfigure.\n\n**Error message**: \n{message}",
|
||||||
|
"device_list_changed_title": "Xiaomi Home device list changes",
|
||||||
|
"device_list_changed": "Detected **{nick_name}({uid}, {cloud_server})** device information has changed, please enter the integration options page, click `Options->Update device list`, update local device information.\n\nCurrent network status: {network_status}\n{message}\n",
|
||||||
|
"device_list_add": "\n**{count} new devices:** \n{message}",
|
||||||
|
"device_list_del": "\n**{count} devices unavailable:** \n{message}",
|
||||||
|
"device_list_offline": "\n**{count} devices offline:** \n{message}",
|
||||||
|
"network_status_online": "Online",
|
||||||
|
"network_status_offline": "Offline",
|
||||||
|
"device_exec_error": "Execution error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"common": {
|
||||||
|
"-10000": "Unknown error",
|
||||||
|
"-10001": "Service unavailable",
|
||||||
|
"-10002": "Invalid parameter",
|
||||||
|
"-10003": "Insufficient resources",
|
||||||
|
"-10004": "Internal error",
|
||||||
|
"-10005": "Insufficient permissions",
|
||||||
|
"-10006": "Execution timeout",
|
||||||
|
"-10007": "Device offline or does not exist",
|
||||||
|
"-10020": "Unauthorized (OAuth2)",
|
||||||
|
"-10030": "Invalid token (HTTP)",
|
||||||
|
"-10040": "Invalid message format",
|
||||||
|
"-10050": "Invalid certificate",
|
||||||
|
"-704000000": "Unknown error",
|
||||||
|
"-704010000": "Unauthorized (device may have been deleted)",
|
||||||
|
"-704014006": "Device description not found",
|
||||||
|
"-704030013": "Property not readable",
|
||||||
|
"-704030023": "Property not writable",
|
||||||
|
"-704030033": "Property not subscribable",
|
||||||
|
"-704040002": "Service does not exist",
|
||||||
|
"-704040003": "Property does not exist",
|
||||||
|
"-704040004": "Event does not exist",
|
||||||
|
"-704040005": "Action does not exist",
|
||||||
|
"-704040999": "Feature not online",
|
||||||
|
"-704042001": "Device does not exist",
|
||||||
|
"-704042011": "Device offline",
|
||||||
|
"-704053036": "Device operation timeout",
|
||||||
|
"-704053100": "Device cannot perform this operation in the current state",
|
||||||
|
"-704083036": "Device operation timeout",
|
||||||
|
"-704090001": "Device does not exist",
|
||||||
|
"-704220008": "Invalid ID",
|
||||||
|
"-704220025": "Action parameter count mismatch",
|
||||||
|
"-704220035": "Action parameter error",
|
||||||
|
"-704220043": "Property value error",
|
||||||
|
"-704222034": "Action return value error",
|
||||||
|
"-705004000": "Unknown error",
|
||||||
|
"-705004501": "Unknown error",
|
||||||
|
"-705201013": "Property not readable",
|
||||||
|
"-705201015": "Action execution error",
|
||||||
|
"-705201023": "Property not writable",
|
||||||
|
"-705201033": "Property not subscribable",
|
||||||
|
"-706012000": "Unknown error",
|
||||||
|
"-706012013": "Property not readable",
|
||||||
|
"-706012015": "Action execution error",
|
||||||
|
"-706012023": "Property not writable",
|
||||||
|
"-706012033": "Property not subscribable",
|
||||||
|
"-706012043": "Property value error",
|
||||||
|
"-706014006": "Device description not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"other": {
|
||||||
|
"devices": "dispositivos",
|
||||||
|
"found_central_gateway": ", se encontró la puerta de enlace central local"
|
||||||
|
},
|
||||||
|
"control_mode": {
|
||||||
|
"auto": "automático",
|
||||||
|
"cloud": "nube"
|
||||||
|
},
|
||||||
|
"room_name_rule": {
|
||||||
|
"none": "no sincronizar",
|
||||||
|
"home_room": "nombre de la casa y nombre de la habitación (Xiaomi Home Dormitorio)",
|
||||||
|
"room": "nombre de la habitación (Dormitorio)",
|
||||||
|
"home": "nombre de la casa (Xiaomi Home)"
|
||||||
|
},
|
||||||
|
"option_status": {
|
||||||
|
"enable": "habilitar",
|
||||||
|
"disable": "deshabilitar"
|
||||||
|
},
|
||||||
|
"lan_ctrl_config": {
|
||||||
|
"notice_net_dup": "\r\n**[Aviso]** Se detectaron varias tarjetas de red que pueden estar conectadas a la misma red. Por favor, preste atención a la selección.",
|
||||||
|
"net_unavailable": "Interfaz no disponible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"miot": {
|
||||||
|
"client": {
|
||||||
|
"invalid_oauth_info": "La información de autenticación es inválida, la conexión en la nube no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar",
|
||||||
|
"invalid_device_cache": "La información de caché del dispositivo es anormal, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local",
|
||||||
|
"invalid_cert_info": "Certificado de usuario inválido, la conexión del centro local no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar",
|
||||||
|
"device_cloud_error": "Error al obtener la información del dispositivo desde la nube, por favor, compruebe la conexión de red local",
|
||||||
|
"xiaomi_home_error_title": "Error de integración de Xiaomi Home",
|
||||||
|
"xiaomi_home_error": "Se detectó un error en **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de opciones para reconfigurar.\n\n**Mensaje de error**: \n{message}",
|
||||||
|
"device_list_changed_title": "Cambio en la lista de dispositivos de Xiaomi Home",
|
||||||
|
"device_list_changed": "Se detectó un cambio en la información del dispositivo **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de integración, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local.\n\nEstado actual de la red: {network_status}\n{message}\n",
|
||||||
|
"device_list_add": "\n**{count} nuevos dispositivos:** \n{message}",
|
||||||
|
"device_list_del": "\n**{count} dispositivos no disponibles:** \n{message}",
|
||||||
|
"device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}",
|
||||||
|
"network_status_online": "En línea",
|
||||||
|
"network_status_offline": "Desconectado",
|
||||||
|
"device_exec_error": "Error de ejecución"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"common": {
|
||||||
|
"-10000": "Error desconocido",
|
||||||
|
"-10001": "Servicio no disponible",
|
||||||
|
"-10002": "Parámetro inválido",
|
||||||
|
"-10003": "Recursos insuficientes",
|
||||||
|
"-10004": "Error interno",
|
||||||
|
"-10005": "Permisos insuficientes",
|
||||||
|
"-10006": "Tiempo de ejecución agotado",
|
||||||
|
"-10007": "Dispositivo fuera de línea o no existe",
|
||||||
|
"-10020": "No autorizado (OAuth2)",
|
||||||
|
"-10030": "Token inválido (HTTP)",
|
||||||
|
"-10040": "Formato de mensaje inválido",
|
||||||
|
"-10050": "Certificado inválido",
|
||||||
|
"-704000000": "Error desconocido",
|
||||||
|
"-704010000": "No autorizado (el dispositivo puede haber sido eliminado)",
|
||||||
|
"-704014006": "Descripción del dispositivo no encontrada",
|
||||||
|
"-704030013": "Propiedad no legible",
|
||||||
|
"-704030023": "Propiedad no escribible",
|
||||||
|
"-704030033": "Propiedad no suscribible",
|
||||||
|
"-704040002": "Servicio no existe",
|
||||||
|
"-704040003": "Propiedad no existe",
|
||||||
|
"-704040004": "Evento no existe",
|
||||||
|
"-704040005": "Acción no existe",
|
||||||
|
"-704040999": "Función no en línea",
|
||||||
|
"-704042001": "Dispositivo no existe",
|
||||||
|
"-704042011": "Dispositivo fuera de línea",
|
||||||
|
"-704053036": "Tiempo de operación del dispositivo agotado",
|
||||||
|
"-704053100": "El dispositivo no puede realizar esta operación en el estado actual",
|
||||||
|
"-704083036": "Tiempo de operación del dispositivo agotado",
|
||||||
|
"-704090001": "Dispositivo no existe",
|
||||||
|
"-704220008": "ID inválido",
|
||||||
|
"-704220025": "Número de parámetros de acción no coincide",
|
||||||
|
"-704220035": "Error de parámetro de acción",
|
||||||
|
"-704220043": "Error de valor de propiedad",
|
||||||
|
"-704222034": "Error de valor de retorno de acción",
|
||||||
|
"-705004000": "Error desconocido",
|
||||||
|
"-705004501": "Error desconocido",
|
||||||
|
"-705201013": "Propiedad no legible",
|
||||||
|
"-705201015": "Error de ejecución de acción",
|
||||||
|
"-705201023": "Propiedad no escribible",
|
||||||
|
"-705201033": "Propiedad no suscribible",
|
||||||
|
"-706012000": "Error desconocido",
|
||||||
|
"-706012013": "Propiedad no legible",
|
||||||
|
"-706012015": "Error de ejecución de acción",
|
||||||
|
"-706012023": "Propiedad no escribible",
|
||||||
|
"-706012033": "Propiedad no suscribible",
|
||||||
|
"-706012043": "Error de valor de propiedad",
|
||||||
|
"-706014006": "Descripción del dispositivo no encontrada"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"other": {
|
||||||
|
"devices": "appareils",
|
||||||
|
"found_central_gateway": ", passerelle centrale locale trouvée"
|
||||||
|
},
|
||||||
|
"control_mode": {
|
||||||
|
"auto": "automatique",
|
||||||
|
"cloud": "cloud"
|
||||||
|
},
|
||||||
|
"room_name_rule": {
|
||||||
|
"none": "ne pas synchroniser",
|
||||||
|
"home_room": "nom de la maison et nom de la pièce (Xiaomi Home Chambre)",
|
||||||
|
"room": "nom de la pièce (Chambre)",
|
||||||
|
"home": "nom de la maison (Xiaomi Home)"
|
||||||
|
},
|
||||||
|
"option_status": {
|
||||||
|
"enable": "activer",
|
||||||
|
"disable": "désactiver"
|
||||||
|
},
|
||||||
|
"lan_ctrl_config": {
|
||||||
|
"notice_net_dup": "\r\n**[Remarque]** Plusieurs cartes réseau détectées qui peuvent être connectées au même réseau. Veuillez faire attention à la sélection.",
|
||||||
|
"net_unavailable": "Interface non disponible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"miot": {
|
||||||
|
"client": {
|
||||||
|
"invalid_oauth_info": "Informations d'authentification non valides, le lien cloud ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \"Options\" pour vous réauthentifier",
|
||||||
|
"invalid_device_cache": "Informations de cache de périphérique non valides, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils",
|
||||||
|
"invalid_cert_info": "Certificat utilisateur non valide, le lien central local ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \"Options\" pour vous réauthentifier",
|
||||||
|
"device_cloud_error": "Erreur lors de la récupération des informations de l'appareil à partir du cloud, veuillez vérifier la connexion réseau locale",
|
||||||
|
"xiaomi_home_error_title": "Erreur d'intégration Xiaomi Home",
|
||||||
|
"xiaomi_home_error": "Erreur détectée sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'options pour reconfigurer.\n\n**Message d'erreur**: \n{message}",
|
||||||
|
"device_list_changed_title": "Changements dans la liste des appareils Xiaomi Home",
|
||||||
|
"device_list_changed": "Changements détectés sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'intégration, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils.\n\nÉtat actuel du réseau : {network_status}\n{message}\n",
|
||||||
|
"device_list_add": "\n**{count} nouveaux appareils :** \n{message}",
|
||||||
|
"device_list_del": "\n**{count} appareils non disponibles :** \n{message}",
|
||||||
|
"device_list_offline": "\n**{count} appareils hors ligne :** \n{message}",
|
||||||
|
"network_status_online": "En ligne",
|
||||||
|
"network_status_offline": "Hors ligne",
|
||||||
|
"device_exec_error": "Erreur d'exécution"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"common": {
|
||||||
|
"-10000": "Erreur inconnue",
|
||||||
|
"-10001": "Service indisponible",
|
||||||
|
"-10002": "Paramètre invalide",
|
||||||
|
"-10003": "Ressources insuffisantes",
|
||||||
|
"-10004": "Erreur interne",
|
||||||
|
"-10005": "Permissions insuffisantes",
|
||||||
|
"-10006": "Délai d'exécution dépassé",
|
||||||
|
"-10007": "Appareil hors ligne ou n'existe pas",
|
||||||
|
"-10020": "Non autorisé (OAuth2)",
|
||||||
|
"-10030": "Jeton invalide (HTTP)",
|
||||||
|
"-10040": "Format de message invalide",
|
||||||
|
"-10050": "Certificat invalide",
|
||||||
|
"-704000000": "Erreur inconnue",
|
||||||
|
"-704010000": "Non autorisé (l'appareil peut avoir été supprimé)",
|
||||||
|
"-704014006": "Description de l'appareil introuvable",
|
||||||
|
"-704030013": "Propriété non lisible",
|
||||||
|
"-704030023": "Propriété non inscriptible",
|
||||||
|
"-704030033": "Propriété non abonnable",
|
||||||
|
"-704040002": "Service n'existe pas",
|
||||||
|
"-704040003": "Propriété n'existe pas",
|
||||||
|
"-704040004": "Événement n'existe pas",
|
||||||
|
"-704040005": "Action n'existe pas",
|
||||||
|
"-704040999": "Fonction non en ligne",
|
||||||
|
"-704042001": "Appareil n'existe pas",
|
||||||
|
"-704042011": "Appareil hors ligne",
|
||||||
|
"-704053036": "Délai d'opération de l'appareil dépassé",
|
||||||
|
"-704053100": "L'appareil ne peut pas effectuer cette opération dans l'état actuel",
|
||||||
|
"-704083036": "Délai d'opération de l'appareil dépassé",
|
||||||
|
"-704090001": "Appareil n'existe pas",
|
||||||
|
"-704220008": "ID invalide",
|
||||||
|
"-704220025": "Nombre de paramètres d'action ne correspond pas",
|
||||||
|
"-704220035": "Erreur de paramètre d'action",
|
||||||
|
"-704220043": "Erreur de valeur de propriété",
|
||||||
|
"-704222034": "Erreur de valeur de retour d'action",
|
||||||
|
"-705004000": "Erreur inconnue",
|
||||||
|
"-705004501": "Erreur inconnue",
|
||||||
|
"-705201013": "Propriété non lisible",
|
||||||
|
"-705201015": "Erreur d'exécution d'action",
|
||||||
|
"-705201023": "Propriété non inscriptible",
|
||||||
|
"-705201033": "Propriété non abonnable",
|
||||||
|
"-706012000": "Erreur inconnue",
|
||||||
|
"-706012013": "Propriété non lisible",
|
||||||
|
"-706012015": "Erreur d'exécution d'action",
|
||||||
|
"-706012023": "Propriété non inscriptible",
|
||||||
|
"-706012033": "Propriété non abonnable",
|
||||||
|
"-706012043": "Erreur de valeur de propriété",
|
||||||
|
"-706014006": "Description de l'appareil introuvable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,142 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT error code and exception.
|
||||||
|
"""
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTErrorCode(Enum):
|
||||||
|
"""MIoT error code."""
|
||||||
|
# Base error code
|
||||||
|
CODE_UNKNOWN = -10000
|
||||||
|
CODE_UNAVAILABLE = -10001
|
||||||
|
CODE_INVALID_PARAMS = -10002
|
||||||
|
CODE_RESOURCE_ERROR = -10003
|
||||||
|
CODE_INTERNAL_ERROR = -10004
|
||||||
|
CODE_UNAUTHORIZED_ACCESS = -10005
|
||||||
|
CODE_TIMEOUT = -10006
|
||||||
|
# OAuth error code
|
||||||
|
CODE_OAUTH_UNAUTHORIZED = -10020
|
||||||
|
# Http error code
|
||||||
|
CODE_HTTP_INVALID_ACCESS_TOKEN = -10030
|
||||||
|
# MIoT mips error code
|
||||||
|
CODE_MIPS_INVALID_RESULT = -10040
|
||||||
|
# MIoT cert error code
|
||||||
|
CODE_CERT_INVALID_CERT = -10050
|
||||||
|
# MIoT spec error code, -10060
|
||||||
|
# MIoT storage error code, -10070
|
||||||
|
# MIoT ev error code, -10080
|
||||||
|
# Mips service error code, -10090
|
||||||
|
# Config flow error code, -10100
|
||||||
|
# Options flow error code , -10110
|
||||||
|
# MIoT lan error code, -10120
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTError(Exception):
|
||||||
|
"""MIoT error."""
|
||||||
|
code: MIoTErrorCode
|
||||||
|
message: any
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, message: any, code: MIoTErrorCode = MIoTErrorCode.CODE_UNKNOWN
|
||||||
|
) -> None:
|
||||||
|
self.message = message
|
||||||
|
self.code = code
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
return f'{{"code":{self.code.value},"message":"{self.message}"}}'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {"code": self.code.value, "message": self.message}
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTOauthError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTHttpError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTMipsError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTDeviceError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTSpecError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTStorageError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTCertError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTClientError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTEvError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MipsServiceError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTConfigError(MIoTError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTOptionsError(MIoTError):
|
||||||
|
...
|
||||||
@ -0,0 +1,320 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT event loop.
|
||||||
|
"""
|
||||||
|
import selectors
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from typing import Callable, TypeVar
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .miot_error import MIoTEvError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TimeoutHandle = TypeVar('TimeoutHandle')
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTFdHandler:
|
||||||
|
"""File descriptor handler."""
|
||||||
|
fd: int
|
||||||
|
read_handler: Callable[[any], None]
|
||||||
|
read_handler_ctx: any
|
||||||
|
write_handler: Callable[[any], None]
|
||||||
|
write_handler_ctx: any
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, fd: int,
|
||||||
|
read_handler: Callable[[any], None] = None,
|
||||||
|
read_handler_ctx: any = None,
|
||||||
|
write_handler: Callable[[any], None] = None,
|
||||||
|
write_handler_ctx: any = None
|
||||||
|
) -> None:
|
||||||
|
self.fd = fd
|
||||||
|
self.read_handler = read_handler
|
||||||
|
self.read_handler_ctx = read_handler_ctx
|
||||||
|
self.write_handler = write_handler
|
||||||
|
self.write_handler_ctx = write_handler_ctx
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTTimeout:
|
||||||
|
"""Timeout handler."""
|
||||||
|
key: TimeoutHandle
|
||||||
|
target: int
|
||||||
|
handler: Callable[[any], None]
|
||||||
|
handler_ctx: any
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, key: str = None, target: int = None,
|
||||||
|
handler: Callable[[any], None] = None,
|
||||||
|
handler_ctx: any = None
|
||||||
|
) -> None:
|
||||||
|
self.key = key
|
||||||
|
self.target = target
|
||||||
|
self.handler = handler
|
||||||
|
self.handler_ctx = handler_ctx
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.target < other.target
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTEventLoop:
|
||||||
|
"""MIoT event loop."""
|
||||||
|
_poll_fd: selectors.DefaultSelector
|
||||||
|
|
||||||
|
_fd_handlers: dict[str, MIoTFdHandler]
|
||||||
|
|
||||||
|
_timer_heap: list[MIoTTimeout]
|
||||||
|
_timer_handlers: dict[str, MIoTTimeout]
|
||||||
|
_timer_handle_seed: int
|
||||||
|
|
||||||
|
# Label if the current fd handler is freed inside a read handler to
|
||||||
|
# avoid invalid reading.
|
||||||
|
_fd_handler_freed_in_read_handler: bool
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._poll_fd = selectors.DefaultSelector()
|
||||||
|
self._timer_heap = []
|
||||||
|
self._timer_handlers = {}
|
||||||
|
self._timer_handle_seed = 1
|
||||||
|
self._fd_handlers = {}
|
||||||
|
self._fd_handler_freed_in_read_handler = False
|
||||||
|
|
||||||
|
def loop_forever(self) -> None:
|
||||||
|
"""Run an event loop in current thread."""
|
||||||
|
next_timeout: int
|
||||||
|
while True:
|
||||||
|
next_timeout = 0
|
||||||
|
# Handle timer
|
||||||
|
now_ms: int = self.__get_monotonic_ms
|
||||||
|
while len(self._timer_heap) > 0:
|
||||||
|
timer: MIoTTimeout = self._timer_heap[0]
|
||||||
|
if timer is None:
|
||||||
|
break
|
||||||
|
if timer.target <= now_ms:
|
||||||
|
heapq.heappop(self._timer_heap)
|
||||||
|
del self._timer_handlers[timer.key]
|
||||||
|
if timer.handler:
|
||||||
|
timer.handler(timer.handler_ctx)
|
||||||
|
else:
|
||||||
|
next_timeout = timer.target-now_ms
|
||||||
|
break
|
||||||
|
# Are there any files to listen to
|
||||||
|
if next_timeout == 0 and self._fd_handlers:
|
||||||
|
next_timeout = None # None == infinite
|
||||||
|
# Wait for timers & fds
|
||||||
|
if next_timeout == 0:
|
||||||
|
# Neither timer nor fds exist, exit loop
|
||||||
|
break
|
||||||
|
# Handle fd event
|
||||||
|
events = self._poll_fd.select(
|
||||||
|
timeout=next_timeout/1000.0 if next_timeout else next_timeout)
|
||||||
|
for key, mask in events:
|
||||||
|
fd_handler: MIoTFdHandler = key.data
|
||||||
|
if fd_handler is None:
|
||||||
|
continue
|
||||||
|
self._fd_handler_freed_in_read_handler = False
|
||||||
|
fd_key = str(id(fd_handler.fd))
|
||||||
|
if fd_key not in self._fd_handlers:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
mask & selectors.EVENT_READ > 0
|
||||||
|
and fd_handler.read_handler
|
||||||
|
):
|
||||||
|
fd_handler.read_handler(fd_handler.read_handler_ctx)
|
||||||
|
if (
|
||||||
|
mask & selectors.EVENT_WRITE > 0
|
||||||
|
and self._fd_handler_freed_in_read_handler is False
|
||||||
|
and fd_handler.write_handler
|
||||||
|
):
|
||||||
|
fd_handler.write_handler(fd_handler.write_handler_ctx)
|
||||||
|
|
||||||
|
def loop_stop(self) -> None:
|
||||||
|
"""Stop the event loop."""
|
||||||
|
if self._poll_fd:
|
||||||
|
self._poll_fd.close()
|
||||||
|
self._poll_fd = None
|
||||||
|
self._fd_handlers = {}
|
||||||
|
self._timer_heap = []
|
||||||
|
self._timer_handlers = {}
|
||||||
|
|
||||||
|
def set_timeout(
|
||||||
|
self, timeout_ms: int, handler: Callable[[any], None],
|
||||||
|
handler_ctx: any = None
|
||||||
|
) -> TimeoutHandle:
|
||||||
|
"""Set a timer."""
|
||||||
|
if timeout_ms is None or handler is None:
|
||||||
|
raise MIoTEvError('invalid params')
|
||||||
|
new_timeout: MIoTTimeout = MIoTTimeout()
|
||||||
|
new_timeout.key = self.__get_next_timeout_handle
|
||||||
|
new_timeout.target = self.__get_monotonic_ms + timeout_ms
|
||||||
|
new_timeout.handler = handler
|
||||||
|
new_timeout.handler_ctx = handler_ctx
|
||||||
|
heapq.heappush(self._timer_heap, new_timeout)
|
||||||
|
self._timer_handlers[new_timeout.key] = new_timeout
|
||||||
|
return new_timeout.key
|
||||||
|
|
||||||
|
def clear_timeout(self, timer_key: TimeoutHandle) -> None:
|
||||||
|
"""Stop and remove the timer."""
|
||||||
|
if timer_key is None:
|
||||||
|
return
|
||||||
|
timer: MIoTTimeout = self._timer_handlers.pop(timer_key, None)
|
||||||
|
if timer:
|
||||||
|
self._timer_heap = list(self._timer_heap)
|
||||||
|
self._timer_heap.remove(timer)
|
||||||
|
heapq.heapify(self._timer_heap)
|
||||||
|
|
||||||
|
def set_read_handler(
|
||||||
|
self, fd: int, handler: Callable[[any], None], handler_ctx: any = None
|
||||||
|
) -> bool:
|
||||||
|
"""Set a read handler for a file descriptor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, success. False, failed.
|
||||||
|
"""
|
||||||
|
self.__set_handler(
|
||||||
|
fd, is_read=True, handler=handler, handler_ctx=handler_ctx)
|
||||||
|
|
||||||
|
def set_write_handler(
|
||||||
|
self, fd: int, handler: Callable[[any], None], handler_ctx: any = None
|
||||||
|
) -> bool:
|
||||||
|
"""Set a write handler for a file descriptor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, success. False, failed.
|
||||||
|
"""
|
||||||
|
self.__set_handler(
|
||||||
|
fd, is_read=False, handler=handler, handler_ctx=handler_ctx)
|
||||||
|
|
||||||
|
def __set_handler(
|
||||||
|
self, fd, is_read: bool, handler: Callable[[any], None],
|
||||||
|
handler_ctx: any = None
|
||||||
|
) -> bool:
|
||||||
|
"""Set a handler."""
|
||||||
|
if fd is None:
|
||||||
|
raise MIoTEvError('invalid params')
|
||||||
|
|
||||||
|
fd_key: str = str(id(fd))
|
||||||
|
fd_handler = self._fd_handlers.get(fd_key, None)
|
||||||
|
|
||||||
|
if fd_handler is None:
|
||||||
|
fd_handler = MIoTFdHandler(fd=fd)
|
||||||
|
fd_handler.fd = fd
|
||||||
|
self._fd_handlers[fd_key] = fd_handler
|
||||||
|
|
||||||
|
read_handler_existed = fd_handler.read_handler is not None
|
||||||
|
write_handler_existed = fd_handler.write_handler is not None
|
||||||
|
if is_read is True:
|
||||||
|
fd_handler.read_handler = handler
|
||||||
|
fd_handler.read_handler_ctx = handler_ctx
|
||||||
|
else:
|
||||||
|
fd_handler.write_handler = handler
|
||||||
|
fd_handler.write_handler_ctx = handler_ctx
|
||||||
|
|
||||||
|
if fd_handler.read_handler is None and fd_handler.write_handler is None:
|
||||||
|
# Remove from epoll and map
|
||||||
|
try:
|
||||||
|
self._poll_fd.unregister(fd)
|
||||||
|
except (KeyError, ValueError, OSError) as e:
|
||||||
|
del e
|
||||||
|
self._fd_handlers.pop(fd_key, None)
|
||||||
|
# May be inside a read handler, if not, this has no effect
|
||||||
|
self._fd_handler_freed_in_read_handler = True
|
||||||
|
elif read_handler_existed is False and write_handler_existed is False:
|
||||||
|
# Add to epoll
|
||||||
|
events = 0x0
|
||||||
|
if fd_handler.read_handler:
|
||||||
|
events |= selectors.EVENT_READ
|
||||||
|
if fd_handler.write_handler:
|
||||||
|
events |= selectors.EVENT_WRITE
|
||||||
|
try:
|
||||||
|
self._poll_fd.register(fd, events=events, data=fd_handler)
|
||||||
|
except (KeyError, ValueError, OSError) as e:
|
||||||
|
_LOGGER.error(
|
||||||
|
'%s, register fd, error, %s, %s, %s, %s, %s',
|
||||||
|
threading.current_thread().name,
|
||||||
|
'read' if is_read else 'write',
|
||||||
|
fd_key, handler, e, traceback.format_exc())
|
||||||
|
self._fd_handlers.pop(fd_key, None)
|
||||||
|
return False
|
||||||
|
elif (
|
||||||
|
read_handler_existed != (fd_handler.read_handler is not None)
|
||||||
|
or write_handler_existed != (fd_handler.write_handler is not None)
|
||||||
|
):
|
||||||
|
# Modify epoll
|
||||||
|
events = 0x0
|
||||||
|
if fd_handler.read_handler:
|
||||||
|
events |= selectors.EVENT_READ
|
||||||
|
if fd_handler.write_handler:
|
||||||
|
events |= selectors.EVENT_WRITE
|
||||||
|
try:
|
||||||
|
self._poll_fd.modify(fd, events=events, data=fd_handler)
|
||||||
|
except (KeyError, ValueError, OSError) as e:
|
||||||
|
_LOGGER.error(
|
||||||
|
'%s, modify fd, error, %s, %s, %s, %s, %s',
|
||||||
|
threading.current_thread().name,
|
||||||
|
'read' if is_read else 'write',
|
||||||
|
fd_key, handler, e, traceback.format_exc())
|
||||||
|
self._fd_handlers.pop(fd_key, None)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __get_next_timeout_handle(self) -> str:
|
||||||
|
# Get next timeout handle, that is not larger than the maximum
|
||||||
|
# value of UINT64 type.
|
||||||
|
self._timer_handle_seed += 1
|
||||||
|
# uint64 max
|
||||||
|
self._timer_handle_seed %= 0xFFFFFFFFFFFFFFFF
|
||||||
|
return str(self._timer_handle_seed)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __get_monotonic_ms(self) -> int:
|
||||||
|
"""Get monotonic ms timestamp."""
|
||||||
|
return int(time.monotonic()*1000)
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT internationalization translation.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .common import load_json_file
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTI18n:
|
||||||
|
"""MIoT Internationalization Translation.
|
||||||
|
Translate by Copilot, which does not guarantee the accuracy of the
|
||||||
|
translation. If there is a problem with the translation, please submit
|
||||||
|
the ISSUE feedback. After the review, we will modify it as soon as possible.
|
||||||
|
"""
|
||||||
|
_main_loop: asyncio.AbstractEventLoop
|
||||||
|
_lang: str
|
||||||
|
_data: dict
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, lang: str, loop: Optional[asyncio.AbstractEventLoop]
|
||||||
|
) -> None:
|
||||||
|
self._main_loop = loop or asyncio.get_event_loop()
|
||||||
|
self._lang = lang
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
async def init_async(self) -> None:
|
||||||
|
if self._data:
|
||||||
|
return
|
||||||
|
data = None
|
||||||
|
self._data = {}
|
||||||
|
try:
|
||||||
|
data = await self._main_loop.run_in_executor(
|
||||||
|
None, load_json_file,
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
f'i18n/{self._lang}.json'))
|
||||||
|
except Exception as err: # pylint: disable=broad-exception-caught
|
||||||
|
_LOGGER.error('load file error, %s', err)
|
||||||
|
return
|
||||||
|
# Check if the file is a valid JSON file
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
_LOGGER.error('valid file, %s', data)
|
||||||
|
return
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
async def deinit_async(self) -> None:
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
def translate(
|
||||||
|
self, key: str, replace: Optional[dict[str, str]] = None
|
||||||
|
) -> str | dict | None:
|
||||||
|
result = self._data
|
||||||
|
for item in key.split('.'):
|
||||||
|
if item not in result:
|
||||||
|
return None
|
||||||
|
result = result[item]
|
||||||
|
if isinstance(result, str) and replace:
|
||||||
|
for k, v in replace.items():
|
||||||
|
result = result.replace('{'+k+'}', str(v))
|
||||||
|
return result or None
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,283 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT central hub gateway service discovery.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import copy
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from zeroconf import (
|
||||||
|
DNSQuestionType,
|
||||||
|
IPVersion,
|
||||||
|
ServiceStateChange,
|
||||||
|
Zeroconf)
|
||||||
|
from zeroconf.asyncio import (
|
||||||
|
AsyncServiceInfo,
|
||||||
|
AsyncZeroconf,
|
||||||
|
AsyncServiceBrowser)
|
||||||
|
|
||||||
|
from .miot_error import MipsServiceError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MIPS_MDNS_TYPE = '_miot-central._tcp.local.'
|
||||||
|
MIPS_MDNS_REQUEST_TIMEOUT_MS = 5000
|
||||||
|
MIPS_MDNS_UPDATE_INTERVAL_S = 600
|
||||||
|
|
||||||
|
|
||||||
|
class MipsServiceState(Enum):
|
||||||
|
ADDED = 1
|
||||||
|
REMOVED = 2
|
||||||
|
UPDATED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class MipsServiceData:
|
||||||
|
"""Mips service data."""
|
||||||
|
profile: str
|
||||||
|
profile_bin: bytes
|
||||||
|
|
||||||
|
name: str
|
||||||
|
addresses: list[str]
|
||||||
|
port: int
|
||||||
|
type: str
|
||||||
|
server: str
|
||||||
|
|
||||||
|
did: str
|
||||||
|
group_id: str
|
||||||
|
role: int
|
||||||
|
suite_mqtt: bool
|
||||||
|
|
||||||
|
def __init__(self, service_info: AsyncServiceInfo) -> None:
|
||||||
|
if service_info is None:
|
||||||
|
raise MipsServiceError('invalid params')
|
||||||
|
properties = service_info.decoded_properties
|
||||||
|
if properties is None:
|
||||||
|
raise MipsServiceError('invalid service properties')
|
||||||
|
self.profile = properties.get('profile', None)
|
||||||
|
if self.profile is None:
|
||||||
|
raise MipsServiceError('invalid service profile')
|
||||||
|
self.profile_bin = base64.b64decode(self.profile)
|
||||||
|
self.name = service_info.name
|
||||||
|
self.addresses = service_info.parsed_addresses(
|
||||||
|
version=IPVersion.V4Only)
|
||||||
|
if not self.addresses:
|
||||||
|
raise MipsServiceError('invalid addresses')
|
||||||
|
self.addresses.sort()
|
||||||
|
self.port = service_info.port
|
||||||
|
self.type = service_info.type
|
||||||
|
self.server = service_info.server
|
||||||
|
# Parse profile
|
||||||
|
self.did = str(int.from_bytes(self.profile_bin[1:9]))
|
||||||
|
self.group_id = binascii.hexlify(
|
||||||
|
self.profile_bin[9:17][::-1]).decode('utf-8')
|
||||||
|
self.role = int(self.profile_bin[20] >> 4)
|
||||||
|
self.suite_mqtt = ((self.profile_bin[22] >> 1) & 0x01) == 0x01
|
||||||
|
|
||||||
|
def valid_service(self) -> bool:
|
||||||
|
if self.role != 1:
|
||||||
|
return False
|
||||||
|
return self.suite_mqtt
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'addresses': self.addresses,
|
||||||
|
'port': self.port,
|
||||||
|
'type': self.type,
|
||||||
|
'server': self.server,
|
||||||
|
'did': self.did,
|
||||||
|
'group_id': self.group_id,
|
||||||
|
'role': self.role,
|
||||||
|
'suite_mqtt': self.suite_mqtt
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
class MipsService:
|
||||||
|
"""MIPS service discovery."""
|
||||||
|
_aiozc: AsyncZeroconf
|
||||||
|
_main_loop: asyncio.AbstractEventLoop
|
||||||
|
_aio_browser: AsyncServiceBrowser
|
||||||
|
_services: dict[str, dict]
|
||||||
|
# key = (key, group_id)
|
||||||
|
_sub_list: dict[(str, str), Callable[[
|
||||||
|
str, MipsServiceState, dict], asyncio.Future]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, aiozc: AsyncZeroconf,
|
||||||
|
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
) -> None:
|
||||||
|
self._aiozc = aiozc
|
||||||
|
self._main_loop = loop or asyncio.get_running_loop()
|
||||||
|
self._aio_browser = None
|
||||||
|
|
||||||
|
self._services = {}
|
||||||
|
self._sub_list = {}
|
||||||
|
|
||||||
|
async def init_async(self) -> None:
|
||||||
|
await self._aiozc.zeroconf.async_wait_for_start()
|
||||||
|
|
||||||
|
self._aio_browser = AsyncServiceBrowser(
|
||||||
|
zeroconf=self._aiozc.zeroconf,
|
||||||
|
type_=MIPS_MDNS_TYPE,
|
||||||
|
handlers=[self.__on_service_state_change],
|
||||||
|
question_type=DNSQuestionType.QM)
|
||||||
|
|
||||||
|
async def deinit_async(self) -> None:
|
||||||
|
await self._aio_browser.async_cancel()
|
||||||
|
self._services = {}
|
||||||
|
self._sub_list = {}
|
||||||
|
|
||||||
|
def get_services(self, group_id: Optional[str] = None) -> dict[str, dict]:
|
||||||
|
"""get mips services.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group_id (str, optional): _description_. Defaults to None.
|
||||||
|
|
||||||
|
Returns: {
|
||||||
|
[group_id:str]: {
|
||||||
|
"name": str,
|
||||||
|
"addresses": list[str],
|
||||||
|
"port": number,
|
||||||
|
"type": str,
|
||||||
|
"server": str,
|
||||||
|
"version": int,
|
||||||
|
"did": str,
|
||||||
|
"group_id": str,
|
||||||
|
"role": int,
|
||||||
|
"suite_mqtt": bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if group_id:
|
||||||
|
if group_id not in self._services:
|
||||||
|
return {}
|
||||||
|
return {group_id: copy.deepcopy(self._services[group_id])}
|
||||||
|
return copy.deepcopy(self._services)
|
||||||
|
|
||||||
|
def sub_service_change(
|
||||||
|
self, key: str, group_id: str,
|
||||||
|
handler: Callable[[str, MipsServiceState, dict], asyncio.Future]
|
||||||
|
) -> None:
|
||||||
|
if key is None or group_id is None or handler is None:
|
||||||
|
raise MipsServiceError('invalid params')
|
||||||
|
self._sub_list[(key, group_id)] = handler
|
||||||
|
|
||||||
|
def unsub_service_change(self, key: str) -> None:
|
||||||
|
if key is None:
|
||||||
|
return
|
||||||
|
for keys in list(self._sub_list.keys()):
|
||||||
|
if key == keys[0]:
|
||||||
|
self._sub_list.pop(keys, None)
|
||||||
|
|
||||||
|
def __on_service_state_change(
|
||||||
|
self, zeroconf: Zeroconf, service_type: str, name: str,
|
||||||
|
state_change: ServiceStateChange
|
||||||
|
) -> None:
|
||||||
|
_LOGGER.debug(
|
||||||
|
'mips service state changed, %s, %s, %s',
|
||||||
|
state_change, name, service_type)
|
||||||
|
|
||||||
|
if state_change is ServiceStateChange.Removed:
|
||||||
|
for item in list(self._services.values()):
|
||||||
|
if item['name'] != name:
|
||||||
|
continue
|
||||||
|
service_data = self._services.pop(item['group_id'], None)
|
||||||
|
self.__call_service_change(
|
||||||
|
state=MipsServiceState.REMOVED, data=service_data)
|
||||||
|
return
|
||||||
|
self._main_loop.create_task(
|
||||||
|
self.__request_service_info_async(zeroconf, service_type, name))
|
||||||
|
|
||||||
|
async def __request_service_info_async(
|
||||||
|
self, zeroconf: Zeroconf, service_type: str, name: str
|
||||||
|
) -> None:
|
||||||
|
info = AsyncServiceInfo(service_type, name)
|
||||||
|
await info.async_request(
|
||||||
|
zeroconf, MIPS_MDNS_REQUEST_TIMEOUT_MS,
|
||||||
|
question_type=DNSQuestionType.QU)
|
||||||
|
|
||||||
|
try:
|
||||||
|
service_data = MipsServiceData(info)
|
||||||
|
if not service_data.valid_service():
|
||||||
|
raise MipsServiceError(
|
||||||
|
'no primary role, no support mqtt connection')
|
||||||
|
if service_data.group_id in self._services:
|
||||||
|
# Update mips service
|
||||||
|
buffer_data = self._services[service_data.group_id]
|
||||||
|
if (
|
||||||
|
service_data.did != buffer_data['did']
|
||||||
|
or service_data.addresses != buffer_data['addresses']
|
||||||
|
or service_data.port != buffer_data['port']
|
||||||
|
):
|
||||||
|
self._services[service_data.group_id].update(
|
||||||
|
service_data.to_dict())
|
||||||
|
self.__call_service_change(
|
||||||
|
state=MipsServiceState.UPDATED,
|
||||||
|
data=service_data.to_dict())
|
||||||
|
else:
|
||||||
|
# Add mips service
|
||||||
|
self._services[service_data.group_id] = service_data.to_dict()
|
||||||
|
self.__call_service_change(
|
||||||
|
state=MipsServiceState.ADDED,
|
||||||
|
data=self._services[service_data.group_id])
|
||||||
|
except MipsServiceError as error:
|
||||||
|
_LOGGER.error('invalid mips service, %s, %s', error, info)
|
||||||
|
|
||||||
|
def __call_service_change(
|
||||||
|
self, state: MipsServiceState, data: dict = None
|
||||||
|
) -> None:
|
||||||
|
_LOGGER.info('call service change, %s, %s', state, data)
|
||||||
|
for keys in list(self._sub_list.keys()):
|
||||||
|
if keys[1] in [data['group_id'], '*']:
|
||||||
|
self._main_loop.create_task(
|
||||||
|
self._sub_list[keys](data['group_id'], state, data))
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,295 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT network utilities.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import platform
|
||||||
|
import socket
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
|
import subprocess
|
||||||
|
from typing import Callable, Optional
|
||||||
|
import psutil
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceStatus(Enum):
|
||||||
|
"""Interface status."""
|
||||||
|
ADD = 0
|
||||||
|
UPDATE = auto()
|
||||||
|
REMOVE = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NetworkInfo:
|
||||||
|
"""Network information."""
|
||||||
|
name: str
|
||||||
|
ip: str
|
||||||
|
netmask: str
|
||||||
|
net_seg: str
|
||||||
|
|
||||||
|
|
||||||
|
class MIoTNetwork:
|
||||||
|
"""MIoT network utilities."""
|
||||||
|
PING_ADDRESS_LIST = [
|
||||||
|
'1.2.4.8', # CNNIC sDNS
|
||||||
|
'8.8.8.8', # Google Public DNS
|
||||||
|
'233.5.5.5', # AliDNS
|
||||||
|
'1.1.1.1', # Cloudflare DNS
|
||||||
|
'114.114.114.114', # 114 DNS
|
||||||
|
'208.67.222.222', # OpenDNS
|
||||||
|
'9.9.9.9', # Quad9 DNS
|
||||||
|
]
|
||||||
|
_main_loop: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
|
_refresh_interval: int
|
||||||
|
_refresh_task: asyncio.Task
|
||||||
|
_refresh_timer: asyncio.TimerHandle
|
||||||
|
|
||||||
|
_network_status: bool
|
||||||
|
_network_info: dict[str, NetworkInfo]
|
||||||
|
|
||||||
|
_sub_list_network_status: dict[str, Callable[[bool], asyncio.Future]]
|
||||||
|
_sub_list_network_info: dict[str, Callable[[
|
||||||
|
InterfaceStatus, NetworkInfo], asyncio.Future]]
|
||||||
|
|
||||||
|
_ping_address_priority: int
|
||||||
|
|
||||||
|
_done_event: asyncio.Event
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
) -> None:
|
||||||
|
self._main_loop = loop or asyncio.get_running_loop()
|
||||||
|
|
||||||
|
self._refresh_interval = None
|
||||||
|
self._refresh_task = None
|
||||||
|
self._refresh_timer = None
|
||||||
|
|
||||||
|
self._network_status = False
|
||||||
|
self._network_info = {}
|
||||||
|
|
||||||
|
self._sub_list_network_status = {}
|
||||||
|
self._sub_list_network_info = {}
|
||||||
|
|
||||||
|
self._ping_address_priority = 0
|
||||||
|
|
||||||
|
self._done_event = asyncio.Event()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network_status(self) -> bool:
|
||||||
|
return self._network_status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network_info(self) -> dict[str, NetworkInfo]:
|
||||||
|
return self._network_info
|
||||||
|
|
||||||
|
async def deinit_async(self) -> None:
|
||||||
|
if self._refresh_task:
|
||||||
|
self._refresh_task.cancel()
|
||||||
|
self._refresh_task = None
|
||||||
|
if self._refresh_timer:
|
||||||
|
self._refresh_timer.cancel()
|
||||||
|
self._refresh_timer = None
|
||||||
|
|
||||||
|
self._refresh_interval = None
|
||||||
|
self._network_status = False
|
||||||
|
self._network_info.clear()
|
||||||
|
self._sub_list_network_status.clear()
|
||||||
|
self._sub_list_network_info.clear()
|
||||||
|
self._done_event.clear()
|
||||||
|
|
||||||
|
def sub_network_status(
|
||||||
|
self, key: str, handler: Callable[[bool], asyncio.Future]
|
||||||
|
) -> None:
|
||||||
|
self._sub_list_network_status[key] = handler
|
||||||
|
|
||||||
|
def unsub_network_status(self, key: str) -> None:
|
||||||
|
self._sub_list_network_status.pop(key, None)
|
||||||
|
|
||||||
|
def sub_network_info(
|
||||||
|
self, key: str,
|
||||||
|
handler: Callable[[InterfaceStatus, NetworkInfo], asyncio.Future]
|
||||||
|
) -> None:
|
||||||
|
self._sub_list_network_info[key] = handler
|
||||||
|
|
||||||
|
def unsub_network_info(self, key: str) -> None:
|
||||||
|
self._sub_list_network_info.pop(key, None)
|
||||||
|
|
||||||
|
async def init_async(self, refresh_interval: int = 30) -> bool:
|
||||||
|
self._refresh_interval = refresh_interval
|
||||||
|
self.__refresh_timer_handler()
|
||||||
|
# MUST get network info before starting
|
||||||
|
return await self._done_event.wait()
|
||||||
|
|
||||||
|
async def refresh_async(self) -> None:
|
||||||
|
self.__refresh_timer_handler()
|
||||||
|
|
||||||
|
async def get_network_status_async(self, timeout: int = 6) -> bool:
|
||||||
|
return await self._main_loop.run_in_executor(
|
||||||
|
None, self.__get_network_status, False, timeout)
|
||||||
|
|
||||||
|
async def get_network_info_async(self) -> dict[str, NetworkInfo]:
|
||||||
|
return await self._main_loop.run_in_executor(
|
||||||
|
None, self.__get_network_info)
|
||||||
|
|
||||||
|
def __calc_network_address(self, ip: str, netmask: str) -> str:
|
||||||
|
return str(ipaddress.IPv4Network(
|
||||||
|
f'{ip}/{netmask}', strict=False).network_address)
|
||||||
|
|
||||||
|
def __ping(
|
||||||
|
self, address: Optional[str] = None, timeout: int = 6
|
||||||
|
) -> bool:
|
||||||
|
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
||||||
|
command = ['ping', param, '1', address]
|
||||||
|
try:
|
||||||
|
output = subprocess.run(
|
||||||
|
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
check=True, timeout=timeout)
|
||||||
|
return output.returncode == 0
|
||||||
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __get_network_status(
|
||||||
|
self, with_retry: bool = True, timeout: int = 6
|
||||||
|
) -> bool:
|
||||||
|
if self._ping_address_priority >= len(self.PING_ADDRESS_LIST):
|
||||||
|
self._ping_address_priority = 0
|
||||||
|
|
||||||
|
if self.__ping(
|
||||||
|
self.PING_ADDRESS_LIST[self._ping_address_priority], timeout):
|
||||||
|
return True
|
||||||
|
if not with_retry:
|
||||||
|
return False
|
||||||
|
for index in range(len(self.PING_ADDRESS_LIST)):
|
||||||
|
if index == self._ping_address_priority:
|
||||||
|
continue
|
||||||
|
if self.__ping(self.PING_ADDRESS_LIST[index], timeout):
|
||||||
|
self._ping_address_priority = index
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __get_network_info(self) -> dict[str, NetworkInfo]:
|
||||||
|
interfaces = psutil.net_if_addrs()
|
||||||
|
results: dict[str, NetworkInfo] = {}
|
||||||
|
for name, addresses in interfaces.items():
|
||||||
|
# Skip hassio and docker* interface
|
||||||
|
if name == 'hassio' or name.startswith('docker'):
|
||||||
|
continue
|
||||||
|
for address in addresses:
|
||||||
|
if (
|
||||||
|
address.family != socket.AF_INET
|
||||||
|
or not address.address
|
||||||
|
or not address.netmask
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
# skip lo interface
|
||||||
|
if address.address == '127.0.0.1':
|
||||||
|
continue
|
||||||
|
results[name] = NetworkInfo(
|
||||||
|
name=name,
|
||||||
|
ip=address.address,
|
||||||
|
netmask=address.netmask,
|
||||||
|
net_seg=self.__calc_network_address(
|
||||||
|
address.address, address.netmask))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def __call_network_info_change(
|
||||||
|
self, status: InterfaceStatus, info: NetworkInfo
|
||||||
|
) -> None:
|
||||||
|
for handler in self._sub_list_network_info.values():
|
||||||
|
self._main_loop.create_task(handler(status, info))
|
||||||
|
|
||||||
|
async def __update_status_and_info_async(self, timeout: int = 6) -> None:
|
||||||
|
try:
|
||||||
|
status: bool = await self._main_loop.run_in_executor(
|
||||||
|
None, self.__get_network_status, timeout)
|
||||||
|
infos = await self._main_loop.run_in_executor(
|
||||||
|
None, self.__get_network_info)
|
||||||
|
|
||||||
|
if self._network_status != status:
|
||||||
|
for handler in self._sub_list_network_status.values():
|
||||||
|
self._main_loop.create_task(handler(status))
|
||||||
|
self._network_status = status
|
||||||
|
|
||||||
|
for name in list(self._network_info.keys()):
|
||||||
|
info = infos.pop(name, None)
|
||||||
|
if info:
|
||||||
|
# Update
|
||||||
|
if (
|
||||||
|
info.ip != self._network_info[name].ip
|
||||||
|
or info.netmask != self._network_info[name].netmask
|
||||||
|
):
|
||||||
|
self._network_info[name] = info
|
||||||
|
self.__call_network_info_change(
|
||||||
|
InterfaceStatus.UPDATE, info)
|
||||||
|
else:
|
||||||
|
# Remove
|
||||||
|
self.__call_network_info_change(
|
||||||
|
InterfaceStatus.REMOVE,
|
||||||
|
self._network_info.pop(name, None))
|
||||||
|
# Add
|
||||||
|
for name, info in infos.items():
|
||||||
|
self._network_info[name] = info
|
||||||
|
self.__call_network_info_change(InterfaceStatus.ADD, info)
|
||||||
|
|
||||||
|
if not self._done_event.is_set():
|
||||||
|
self._done_event.set()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
_LOGGER.error('update_status_and_info task was cancelled')
|
||||||
|
|
||||||
|
def __refresh_timer_handler(self) -> None:
|
||||||
|
if self._refresh_timer:
|
||||||
|
self._refresh_timer.cancel()
|
||||||
|
self._refresh_timer = None
|
||||||
|
if self._refresh_task is None or self._refresh_task.done():
|
||||||
|
self._refresh_task = self._main_loop.create_task(
|
||||||
|
self.__update_status_and_info_async())
|
||||||
|
self._refresh_timer = self._main_loop.call_later(
|
||||||
|
self._refresh_interval, self.__refresh_timer_handler)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,235 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"urn:miot-spec-v2:property:air-cooler:000000EB": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:alarm:00000012": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:anion:00000025": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:auto-cleanup:00000124": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:auto-deodorization:00000125": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:auto-keep-warm:0000002B": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:automatic-feeding:000000F0": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:blow:000000CD": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:deodorization:000000C6": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:dns-auto-mode:000000DC": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:dryer:00000027": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:eco:00000024": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:glimmer-full-color:00000089": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:guard-mode:000000B6": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:heater:00000026": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:heating:000000C7": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:horizontal-swing:00000017": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:hot-water-recirculation:0000011C": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:image-distortion-correction:0000010F": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:mute:00000040": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:motion-detection:00000056": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:motion-tracking:0000008A": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:off-delay:00000053": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:on:00000006": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:physical-controls-locked:0000001D": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:preheat:00000103": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:sleep-aid-mode:0000010B": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:sleep-mode:00000028": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:soft-wind:000000CF": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:speed-control:000000E8": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:time-watermark:00000087": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:un-straight-blowing:00000100": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:uv:00000029": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:valve-switch:000000FE": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:ventilation:000000CE": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:vertical-swing:00000018": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:wake-up-mode:00000107": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:water-pump:000000F2": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:watering:000000CC": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:wdr-mode:00000088": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:wet:0000002A": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:wifi-band-combine:000000E0": "open_close",
|
||||||
|
"urn:miot-spec-v2:property:anti-fake:00000130": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:delay:0000014F": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:driving-status:000000B9": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:local-storage:0000011E": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:plasma:00000132": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:seating-state:000000B8": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:snore-state:0000012A": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no",
|
||||||
|
"urn:miot-spec-v2:property:motion-state:0000007D": "motion_state",
|
||||||
|
"urn:miot-spec-v2:property:contact-state:0000007C": "contact_state"
|
||||||
|
},
|
||||||
|
"translate": {
|
||||||
|
"default": {
|
||||||
|
"zh-Hans": {
|
||||||
|
"true": "真",
|
||||||
|
"false": "假"
|
||||||
|
},
|
||||||
|
"zh-Hant": {
|
||||||
|
"true": "真",
|
||||||
|
"false": "假"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"true": "True",
|
||||||
|
"false": "False"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"true": "Wahr",
|
||||||
|
"false": "Falsch"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"true": "Verdadero",
|
||||||
|
"false": "Falso"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"true": "Vrai",
|
||||||
|
"false": "Faux"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"true": "Истина",
|
||||||
|
"false": "Ложь"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"true": "真",
|
||||||
|
"false": "偽"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"open_close": {
|
||||||
|
"zh-Hans": {
|
||||||
|
"true": "开启",
|
||||||
|
"false": "关闭"
|
||||||
|
},
|
||||||
|
"zh-Hant": {
|
||||||
|
"true": "開啟",
|
||||||
|
"false": "關閉"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"true": "Open",
|
||||||
|
"false": "Close"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"true": "Öffnen",
|
||||||
|
"false": "Schließen"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"true": "Abierto",
|
||||||
|
"false": "Cerrado"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"true": "Ouvert",
|
||||||
|
"false": "Fermer"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"true": "Открыть",
|
||||||
|
"false": "Закрыть"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"true": "開く",
|
||||||
|
"false": "閉じる"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yes_no": {
|
||||||
|
"zh-Hans": {
|
||||||
|
"true": "是",
|
||||||
|
"false": "否"
|
||||||
|
},
|
||||||
|
"zh-Hant": {
|
||||||
|
"true": "是",
|
||||||
|
"false": "否"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"true": "Yes",
|
||||||
|
"false": "No"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"true": "Ja",
|
||||||
|
"false": "Nein"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"true": "Sí",
|
||||||
|
"false": "No"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"true": "Oui",
|
||||||
|
"false": "Non"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"true": "Да",
|
||||||
|
"false": "Нет"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"true": "はい",
|
||||||
|
"false": "いいえ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion_state": {
|
||||||
|
"zh-Hans": {
|
||||||
|
"true": "有人",
|
||||||
|
"false": "无人"
|
||||||
|
},
|
||||||
|
"zh-Hant": {
|
||||||
|
"true": "有人",
|
||||||
|
"false": "無人"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"true": "Motion Detected",
|
||||||
|
"false": "No Motion Detected"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"true": "Bewegung erkannt",
|
||||||
|
"false": "Keine Bewegung erkannt"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"true": "Movimiento detectado",
|
||||||
|
"false": "No se detecta movimiento"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"true": "Mouvement détecté",
|
||||||
|
"false": "Aucun mouvement détecté"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"true": "Обнаружено движение",
|
||||||
|
"false": "Движение не обнаружено"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"true": "動きを検知",
|
||||||
|
"false": "動きが検出されません"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact_state": {
|
||||||
|
"zh-Hans": {
|
||||||
|
"true": "接触",
|
||||||
|
"false": "分离"
|
||||||
|
},
|
||||||
|
"zh-Hant": {
|
||||||
|
"true": "接觸",
|
||||||
|
"false": "分離"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"true": "Contact",
|
||||||
|
"false": "No Contact"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"true": "Kontakt",
|
||||||
|
"false": "Kein Kontakt"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"true": "Contacto",
|
||||||
|
"false": "Sin contacto"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"true": "Contact",
|
||||||
|
"false": "Pas de contact"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"true": "Контакт",
|
||||||
|
"false": "Нет контакта"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"true": "接触",
|
||||||
|
"false": "非接触"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
|
||||||
|
"services": [
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": {
|
||||||
|
"services": [
|
||||||
|
"4",
|
||||||
|
"7",
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
"5.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:light:0000A001:philips-strip3": {
|
||||||
|
"services": [
|
||||||
|
"1",
|
||||||
|
"3"
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
"2.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
||||||
|
"events": [
|
||||||
|
"2.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
|
||||||
|
"services": [
|
||||||
|
"1",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:light:0000A001:yeelink-color2": {
|
||||||
|
"properties": [
|
||||||
|
"3.*",
|
||||||
|
"2.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2": {
|
||||||
|
"services": [
|
||||||
|
"3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3": {
|
||||||
|
"services": [
|
||||||
|
"3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
|
||||||
|
"services": [
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
"9.*",
|
||||||
|
"13.*",
|
||||||
|
"15.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,392 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
|
||||||
|
"""
|
||||||
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
|
from homeassistant.components.event import EventDeviceClass
|
||||||
|
|
||||||
|
# pylint: disable=pointless-string-statement
|
||||||
|
"""SPEC_DEVICE_TRANS_MAP
|
||||||
|
{
|
||||||
|
'<device instance name>':{
|
||||||
|
'required':{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||||
|
'humidifier': {
|
||||||
|
'required': {
|
||||||
|
'humidifier': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mode', 'target-humidity'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'environment': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'relative-humidity': {'read', 'write'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'humidifier'
|
||||||
|
},
|
||||||
|
'dehumidifier': {
|
||||||
|
'required': {
|
||||||
|
'dehumidifier': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mode', 'target-humidity'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'environment': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'relative-humidity': {'read', 'write'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'dehumidifier'
|
||||||
|
},
|
||||||
|
'vacuum': {
|
||||||
|
'required': {
|
||||||
|
'vacuum': {
|
||||||
|
'required': {
|
||||||
|
'actions': {'start-sweep', 'stop-sweeping'},
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'status', 'fan-level'},
|
||||||
|
'actions': {
|
||||||
|
'pause-sweeping',
|
||||||
|
'continue-sweep',
|
||||||
|
'stop-and-gocharge'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'identify': {
|
||||||
|
'required': {
|
||||||
|
'actions': {'identify'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'battery': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'battery-level': {'read'}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'entity': 'vacuum'
|
||||||
|
},
|
||||||
|
'air-conditioner': {
|
||||||
|
'required': {
|
||||||
|
'air-conditioner': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'},
|
||||||
|
'mode': {'read', 'write'},
|
||||||
|
'target-temperature': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'target-humidity'}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'fan-control': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'on',
|
||||||
|
'fan-level',
|
||||||
|
'horizontal-swing',
|
||||||
|
'vertical-swing'}}
|
||||||
|
},
|
||||||
|
'environment': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'temperature', 'relative-humidity'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'air-condition-outlet-matching': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'ac-state'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'climate'
|
||||||
|
},
|
||||||
|
'air-condition-outlet': 'air-conditioner'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""SPEC_SERVICE_TRANS_MAP
|
||||||
|
{
|
||||||
|
'<service instance name>':{
|
||||||
|
'required':{
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>': set<property access: str>
|
||||||
|
},
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'optional':{
|
||||||
|
'properties': set<property instance name: str>,
|
||||||
|
'events': set<event instance name: str>,
|
||||||
|
'actions': set<action instance name: str>
|
||||||
|
},
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||||
|
'light': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'mode', 'brightness', 'color', 'color-temperature'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'light'
|
||||||
|
},
|
||||||
|
'indicator-light': 'light',
|
||||||
|
'ambient-light': 'light',
|
||||||
|
'night-light': 'light',
|
||||||
|
'white-light': 'light',
|
||||||
|
'fan': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'},
|
||||||
|
'fan-level': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mode', 'horizontal-swing'}
|
||||||
|
},
|
||||||
|
'entity': 'fan'
|
||||||
|
},
|
||||||
|
'fan-control': 'fan',
|
||||||
|
'ceiling-fan': 'fan',
|
||||||
|
'water-heater': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'on', 'temperature', 'target-temperature', 'mode'}
|
||||||
|
},
|
||||||
|
'entity': 'water_heater'
|
||||||
|
},
|
||||||
|
'curtain': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'motor-control': {'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'motor-control', 'status', 'current-position', 'target-position'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'cover'
|
||||||
|
},
|
||||||
|
'window-opener': 'curtain'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""SPEC_PROP_TRANS_MAP
|
||||||
|
{
|
||||||
|
'entities':{
|
||||||
|
'<entity name>':{
|
||||||
|
'format': set<str>,
|
||||||
|
'access': set<str>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'properties': {
|
||||||
|
'<property instance name>':{
|
||||||
|
'device_class': str,
|
||||||
|
'entity': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||||
|
'entities': {
|
||||||
|
'sensor': {
|
||||||
|
'format': {'int', 'float'},
|
||||||
|
'access': {'read'}
|
||||||
|
},
|
||||||
|
'switch': {
|
||||||
|
'format': {'bool'},
|
||||||
|
'access': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'properties': {
|
||||||
|
'temperature': {
|
||||||
|
'device_class': SensorDeviceClass.TEMPERATURE,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'relative-humidity': {
|
||||||
|
'device_class': SensorDeviceClass.HUMIDITY,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'air-quality-index': {
|
||||||
|
'device_class': SensorDeviceClass.AQI,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'pm2.5-density': {
|
||||||
|
'device_class': SensorDeviceClass.PM25,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'pm10-density': {
|
||||||
|
'device_class': SensorDeviceClass.PM10,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'pm1': {
|
||||||
|
'device_class': SensorDeviceClass.PM1,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'atmospheric-pressure': {
|
||||||
|
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'tvoc-density': {
|
||||||
|
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'voc-density': 'tvoc-density',
|
||||||
|
'battery-level': {
|
||||||
|
'device_class': SensorDeviceClass.BATTERY,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'voltage': {
|
||||||
|
'device_class': SensorDeviceClass.VOLTAGE,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'illumination': {
|
||||||
|
'device_class': SensorDeviceClass.ILLUMINANCE,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'no-one-determine-time': {
|
||||||
|
'device_class': SensorDeviceClass.DURATION,
|
||||||
|
'entity': 'sensor'
|
||||||
|
},
|
||||||
|
'has-someone-duration': 'no-one-determine-time',
|
||||||
|
'no-one-duration': 'no-one-determine-time'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""SPEC_EVENT_TRANS_MAP
|
||||||
|
{
|
||||||
|
'<event instance name>': str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
SPEC_EVENT_TRANS_MAP: dict[str, str] = {
|
||||||
|
'click': EventDeviceClass.BUTTON,
|
||||||
|
'double-click': EventDeviceClass.BUTTON,
|
||||||
|
'long-press': EventDeviceClass.BUTTON,
|
||||||
|
'motion-detected': EventDeviceClass.MOTION,
|
||||||
|
'no-motion': EventDeviceClass.MOTION,
|
||||||
|
'doorbell-ring': EventDeviceClass.DOORBELL
|
||||||
|
}
|
||||||
|
|
||||||
|
SPEC_ACTION_TRANS_MAP = {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
MIoT redirect web pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def oauth_redirect_page(lang: str, status: str) -> str:
|
||||||
|
"""Return oauth redirect page."""
|
||||||
|
return '''
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="https://cdn.web-global.fds.api.mi-img.com/mcfe--mi-account/static/favicon_new.ico">
|
||||||
|
<link as="style"
|
||||||
|
href="https://font.sec.miui.com/font/css?family=MiSans:300,400,500,600,700:Chinese_Simplify,Chinese_Traditional,Latin&display=swap"
|
||||||
|
rel="preload">
|
||||||
|
<title></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: MiSans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame {
|
||||||
|
background: rgb(255 255 255 / 5%);
|
||||||
|
width: 360px;
|
||||||
|
padding: 40 45;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 20px 50px 0 hsla(0, 0%, 64%, .1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.logo-frame {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.title-frame {
|
||||||
|
margin: 20px 0 20px 0;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 40px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.content-frame {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #ff5c00;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 20px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="frame">
|
||||||
|
<!-- XIAOMI LOGO-->
|
||||||
|
<div class="logo-frame">
|
||||||
|
<svg width="50" height="50" viewBox="0 0 193 193" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"><title>编组</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<polygon id="path-1"
|
||||||
|
points="1.78097075e-14 0.000125324675 192.540685 0.000125324675 192.540685 192.540058 1.78097075e-14 192.540058"></polygon>
|
||||||
|
</defs>
|
||||||
|
<g id="\u9875\u9762-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="\u7F16\u7EC4">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Clip-2"></g>
|
||||||
|
<path d="M172.473071,20.1164903 C154.306633,2.02148701 128.188344,-1.78097075e-14 96.2706558,-1.78097075e-14 C64.312237,-1.78097075e-14 38.155724,2.0452987 19.9974318,20.1872987 C1.84352597,38.3261656 1.78097075e-14,64.4406948 1.78097075e-14,96.3640227 C1.78097075e-14,128.286724 1.84352597,154.415039 20.0049513,172.556412 C38.1638701,190.704052 64.3141169,192.540058 96.2706558,192.540058 C128.225942,192.540058 154.376815,190.704052 172.53636,172.556412 C190.694653,154.409399 192.540685,128.286724 192.540685,96.3640227 C192.540685,64.3999643 190.672721,38.2553571 172.473071,20.1164903"
|
||||||
|
id="Fill-1" fill="#FF6900" mask="url(#mask-2)"></path>
|
||||||
|
<path d="M89.1841721,131.948836 C89.1841721,132.594885 88.640263,133.130648 87.9779221,133.130648 L71.5585097,133.130648 C70.8848896,133.130648 70.338474,132.594885 70.338474,131.948836 L70.338474,89.0100961 C70.338474,88.3584078 70.8848896,87.8251513 71.5585097,87.8251513 L87.9779221,87.8251513 C88.640263,87.8251513 89.1841721,88.3584078 89.1841721,89.0100961 L89.1841721,131.948836 Z"
|
||||||
|
id="Fill-3" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||||
|
<path d="M121.332896,131.948836 C121.332896,132.594885 120.786481,133.130648 120.121633,133.130648 L104.492393,133.130648 C103.821906,133.130648 103.275491,132.594885 103.275491,131.948836 L103.275491,131.788421 L103.275491,94.9022357 C103.259198,88.4342292 102.889491,81.7863818 99.5502146,78.445226 C96.6790263,75.5652649 91.3251562,74.9054305 85.7557276,74.7669468 L57.4242049,74.7669468 C56.7555977,74.7669468 56.2154484,75.3045896 56.2154484,75.9512649 L56.2154484,128.074424 L56.2154484,131.948836 C56.2154484,132.594885 55.6640198,133.130648 54.9954127,133.130648 L39.3555198,133.130648 C38.6875393,133.130648 38.1498964,132.594885 38.1498964,131.948836 L38.1498964,60.5996188 C38.1498964,59.9447974 38.6875393,59.4121675 39.3555198,59.4121675 L84.4786692,59.4121675 C96.2717211,59.4121675 108.599909,59.9498104 114.680036,66.0380831 C120.786481,72.1533006 121.332896,84.4595571 121.332896,96.2657682 L121.332896,131.948836 Z"
|
||||||
|
id="Fill-5" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||||
|
<path d="M153.53056,131.948836 C153.53056,132.594885 152.978505,133.130648 152.316164,133.130648 L136.678778,133.130648 C136.010797,133.130648 135.467515,132.594885 135.467515,131.948836 L135.467515,60.5996188 C135.467515,59.9447974 136.010797,59.4121675 136.678778,59.4121675 L152.316164,59.4121675 C152.978505,59.4121675 153.53056,59.9447974 153.53056,60.5996188 L153.53056,131.948836 Z"
|
||||||
|
id="Fill-7" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- TITLE -->
|
||||||
|
<div class="title-frame">
|
||||||
|
<a id="titleArea"></a>
|
||||||
|
</div>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="content-frame">
|
||||||
|
<a id="contentArea"></a>
|
||||||
|
</div>
|
||||||
|
<!-- BUTTON -->
|
||||||
|
<button onClick="window.close();" id="buttonArea"></button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// get language (user language -> system language)
|
||||||
|
const locale = (localStorage.getItem('selectedLanguage')?? "''' + lang + '''").replaceAll('"','');
|
||||||
|
const language = locale.includes("-") ? locale.substring(0, locale.indexOf("-")).trim() : locale;
|
||||||
|
const status = "''' + status + '''";
|
||||||
|
console.log(locale);
|
||||||
|
// translation
|
||||||
|
let translation = {
|
||||||
|
zh: {
|
||||||
|
success: {
|
||||||
|
title: "认证完成",
|
||||||
|
content: "请关闭此页面,返回账号认证页面点击“下一步”",
|
||||||
|
button: "关闭页面"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "认证失败",
|
||||||
|
content: "请关闭此页面,返回账号认证页面重新点击认链接进行认证。",
|
||||||
|
button: "关闭页面"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'zh-Hant': {
|
||||||
|
success: {
|
||||||
|
title: "認證完成",
|
||||||
|
content: "請關閉此頁面,返回帳號認證頁面點擊「下一步」。",
|
||||||
|
button: "關閉頁面"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "認證失敗",
|
||||||
|
content: "請關閉此頁面,返回帳號認證頁面重新點擊認鏈接進行認證。",
|
||||||
|
button: "關閉頁面"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
success: {
|
||||||
|
title: "Authentication Completed",
|
||||||
|
content: "Please close this page and return to the account authentication page to click NEXT",
|
||||||
|
button: "Close Page"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "Authentication Failed",
|
||||||
|
content: "Please close this page and return to the account authentication page to click the authentication link again.",
|
||||||
|
button: "Close Page"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
success: {
|
||||||
|
title: "Authentification Terminée",
|
||||||
|
content: "Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer sur « SUIVANT »",
|
||||||
|
button: "Fermer la page"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "Échec de l'Authentification",
|
||||||
|
content: "Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer de nouveau sur le lien d'authentification.",
|
||||||
|
button: "Fermer la page"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
success: {
|
||||||
|
title: "Подтверждение завершено",
|
||||||
|
content: "Пожалуйста, закройте эту страницу, вернитесь на страницу аутентификации учетной записи и нажмите кнопку «Далее».",
|
||||||
|
button: "Закрыть страницу"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "Ошибка аутентификации",
|
||||||
|
content: "Пожалуйста, закройте эту страницу, вернитесь на страницу аутентификации учетной записи и повторите процесс аутентификации, щелкнув ссылку.",
|
||||||
|
button: "Закрыть страницу"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
success: {
|
||||||
|
title: "Authentifizierung abgeschlossen",
|
||||||
|
content: "Bitte schließen Sie diese Seite, kehren Sie zur Kontobestätigungsseite zurück und klicken Sie auf „WEITER“.",
|
||||||
|
button: "Seite schließen"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "Authentifizierung fehlgeschlagen",
|
||||||
|
content: "Bitte schließen Sie diese Seite, kehren Sie zur Kontobestätigungsseite zurück und wiederholen Sie den Authentifizierungsprozess, indem Sie auf den Link klicken.",
|
||||||
|
button: "Seite schließen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
success: {
|
||||||
|
title: "Autenticación completada",
|
||||||
|
content: "Por favor, cierre esta página, regrese a la página de autenticación de la cuenta y haga clic en 'SIGUIENTE'.",
|
||||||
|
button: "Cerrar página"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "Error de autenticación",
|
||||||
|
content: "Por favor, cierre esta página, regrese a la página de autenticación de la cuenta y vuelva a hacer clic en el enlace de autenticación.",
|
||||||
|
button: "Cerrar página"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
success: {
|
||||||
|
title: "認証完了",
|
||||||
|
content: "このページを閉じて、アカウント認証ページに戻り、「次」をクリックしてください。",
|
||||||
|
button: "ページを閉じる"
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
title: "認証失敗",
|
||||||
|
content: "このページを閉じて、アカウント認証ページに戻り、認証リンクを再度クリックしてください。",
|
||||||
|
button: "ページを閉じる"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// insert translate into page / match order: locale > language > english
|
||||||
|
document.title = translation[locale]?.[status]?.title ?? translation[language]?.[status]?.title ?? translation["en"]?.[status]?.title;
|
||||||
|
document.getElementById("titleArea").innerText = translation[locale]?.[status]?.title ?? translation[language]?.[status]?.title ?? translation["en"]?.[status]?.title;
|
||||||
|
document.getElementById("contentArea").innerText = translation[locale]?.[status]?.content ?? translation[language]?.[status]?.content ?? translation["en"]?.[status]?.content;
|
||||||
|
document.getElementById("buttonArea").innerText = translation[locale]?.[status]?.button ?? translation[language]?.[status]?.button ?? translation["en"]?.[status]?.button;
|
||||||
|
window.opener=null;
|
||||||
|
window.open('','_self');
|
||||||
|
window.close();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Notify entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.notify import NotifyEntity
|
||||||
|
|
||||||
|
from .miot.miot_spec import MIoTSpecAction
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTActionEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for action in miot_device.action_list.get('notify', []):
|
||||||
|
new_entities.append(Notify(miot_device=miot_device, spec=action))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Notify(MIoTActionEntity, NotifyEntity):
|
||||||
|
"""Notify entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
|
||||||
|
"""Initialize the Notify."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
self._attr_extra_state_attributes = {}
|
||||||
|
action_in: str = ', '.join([
|
||||||
|
f'{prop.description_trans}({prop.format_})'
|
||||||
|
for prop in self.spec.in_])
|
||||||
|
self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
|
||||||
|
|
||||||
|
async def async_send_message(
|
||||||
|
self, message: str, title: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Send a message."""
|
||||||
|
del title
|
||||||
|
if not message:
|
||||||
|
_LOGGER.error(
|
||||||
|
'action exec failed, %s(%s), empty action params',
|
||||||
|
self.name, self.entity_id)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
in_list: list = json.loads(message)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
_LOGGER.error(
|
||||||
|
'action exec failed, %s(%s), invalid action params format, %s',
|
||||||
|
self.name, self.entity_id, message)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(in_list, list) or len(in_list) != len(self.spec.in_):
|
||||||
|
_LOGGER.error(
|
||||||
|
'action exec failed, %s(%s), invalid action params, %s',
|
||||||
|
self.name, self.entity_id, message)
|
||||||
|
return
|
||||||
|
|
||||||
|
in_value: list[dict] = []
|
||||||
|
for index, prop in enumerate(self.spec.in_):
|
||||||
|
if type(in_list[index]).__name__ != prop.format_:
|
||||||
|
logging.error(
|
||||||
|
'action exec failed, %s(%s), invalid params item, '
|
||||||
|
'which item(%s) in the list must be %s, %s',
|
||||||
|
self.name, self.entity_id, prop.description_trans,
|
||||||
|
prop.format_, message)
|
||||||
|
return
|
||||||
|
in_value.append({'piid': prop.iid, 'value': in_list[index]})
|
||||||
|
return await self.action_async(in_list=in_value)
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Number entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.number import NumberEntity
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('number', []):
|
||||||
|
new_entities.append(Number(miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Number(MIoTPropertyEntity, NumberEntity):
|
||||||
|
"""Number entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the Notify."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Set device_class
|
||||||
|
self._attr_device_class = spec.device_class
|
||||||
|
# Set unit
|
||||||
|
if self.spec.external_unit:
|
||||||
|
self._attr_native_unit_of_measurement = self.spec.external_unit
|
||||||
|
# Set icon
|
||||||
|
if self.spec.icon:
|
||||||
|
self._attr_icon = self.spec.icon
|
||||||
|
# Set value range
|
||||||
|
if self._value_range:
|
||||||
|
self._attr_native_min_value = self._value_range['min']
|
||||||
|
self._attr_native_max_value = self._value_range['max']
|
||||||
|
self._attr_native_step = self._value_range['step']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> Optional[float]:
|
||||||
|
"""Return the current value of the number."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Update the current value."""
|
||||||
|
await self.set_property_async(value=value)
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Select entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('select', []):
|
||||||
|
new_entities.append(Select(miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Select(MIoTPropertyEntity, SelectEntity):
|
||||||
|
"""Select entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the Select."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
self._attr_options = list(self._value_list.values())
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.set_property_async(
|
||||||
|
value=self.get_vlist_value(description=option))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> Optional[str]:
|
||||||
|
"""Return the current selected option."""
|
||||||
|
return self.get_vlist_description(value=self._value)
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Sensor entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
|
||||||
|
from homeassistant.components.sensor import DEVICE_CLASS_UNITS
|
||||||
|
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('sensor', []):
|
||||||
|
new_entities.append(Sensor(miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||||
|
"""Sensor entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the Sensor."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Set device_class
|
||||||
|
if self._value_list:
|
||||||
|
self._attr_device_class = SensorDeviceClass.ENUM
|
||||||
|
self._attr_icon = 'mdi:message-text'
|
||||||
|
self._attr_native_unit_of_measurement = None
|
||||||
|
self._attr_options = list(self._value_list.values())
|
||||||
|
else:
|
||||||
|
self._attr_device_class = spec.device_class
|
||||||
|
if spec.external_unit:
|
||||||
|
self._attr_native_unit_of_measurement = spec.external_unit
|
||||||
|
else:
|
||||||
|
# device_class is not empty but unit is empty.
|
||||||
|
# Set the default unit according to device_class.
|
||||||
|
unit_sets = DEVICE_CLASS_UNITS.get(
|
||||||
|
self._attr_device_class, None)
|
||||||
|
self._attr_native_unit_of_measurement = list(
|
||||||
|
unit_sets)[0] if unit_sets else None
|
||||||
|
# Set icon
|
||||||
|
if spec.icon:
|
||||||
|
self._attr_icon = spec.icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> any:
|
||||||
|
"""Return the current value of the sensor."""
|
||||||
|
if self._value_range and isinstance(self._value, (int, float)):
|
||||||
|
if (
|
||||||
|
self._value < self._value_range['min']
|
||||||
|
or self._value > self._value_range['max']
|
||||||
|
):
|
||||||
|
_LOGGER.info(
|
||||||
|
'%s, data exception, out of range, %s, %s',
|
||||||
|
self.entity_id, self._value, self._value_range)
|
||||||
|
if self._value_list:
|
||||||
|
return self._value_list.get(self._value, None)
|
||||||
|
if isinstance(self._value, str):
|
||||||
|
return self._value[:255]
|
||||||
|
return self._value
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Switch entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
|
||||||
|
from .miot.miot_device import MIoTDevice
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTPropertyEntity
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('switch', []):
|
||||||
|
new_entities.append(Switch(miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Switch(MIoTPropertyEntity, SwitchEntity):
|
||||||
|
"""Switch entities for Xiaomi Home."""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the Switch."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
# Set device_class
|
||||||
|
self._attr_device_class = spec.device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""On/Off state."""
|
||||||
|
return self._value is True
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
await self.set_property_async(value=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off."""
|
||||||
|
await self.set_property_async(value=False)
|
||||||
|
|
||||||
|
async def async_toggle(self, **kwargs: Any) -> None:
|
||||||
|
"""Toggle the switch."""
|
||||||
|
await self.set_property_async(value=not self.is_on)
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
"""Check if a file is a valid JSON file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python json_check.py [JSON file path]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python json_check.py multi_lang.json
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def check_json_file(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
json.load(file)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(file_path, "is not found.")
|
||||||
|
return False
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(file_path, "is not a valid JSON file.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Check if a file is a valid JSON file.")
|
||||||
|
parser.add_argument("file_path", help="JSON file path")
|
||||||
|
args = parser.parse_args()
|
||||||
|
script_name = os.path.basename(__file__)
|
||||||
|
file_name = os.path.basename(args.file_path)
|
||||||
|
|
||||||
|
if not check_json_file(args.file_path):
|
||||||
|
print(args.file_path, script_name, "FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(script_name, file_name, "PASS")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
"""Check if conversion rules are valid.
|
||||||
|
|
||||||
|
The files to be checked are in the directory of ../miot/specs/
|
||||||
|
To run this script, PYTHONPATH must be set first.
|
||||||
|
See test_all.sh for the usage.
|
||||||
|
|
||||||
|
You can run all tests by running:
|
||||||
|
```
|
||||||
|
./test_all.sh
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
def load_json(file_path: str) -> dict:
|
||||||
|
"""Load json file."""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def dict_str_str(d: dict) -> bool:
|
||||||
|
"""restricted format: dict[str, str]"""
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return False
|
||||||
|
for k, v in d.items():
|
||||||
|
if not isinstance(k, str) or not isinstance(v, str):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dict_str_dict(d: dict) -> bool:
|
||||||
|
"""restricted format: dict[str, dict]"""
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return False
|
||||||
|
for k, v in d.items():
|
||||||
|
if not isinstance(k, str) or not isinstance(v, dict):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def nested_2_dict_str_str(d: dict) -> bool:
|
||||||
|
"""restricted format: dict[str, dict[str, str]]"""
|
||||||
|
if not dict_str_dict(d):
|
||||||
|
return False
|
||||||
|
for v in d.values():
|
||||||
|
if not dict_str_str(v):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def nested_3_dict_str_str(d: dict) -> bool:
|
||||||
|
"""restricted format: dict[str, dict[str, dict[str, str]]]"""
|
||||||
|
if not dict_str_dict(d):
|
||||||
|
return False
|
||||||
|
for v in d.values():
|
||||||
|
if not nested_2_dict_str_str(v):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def spec_filter(d: dict) -> bool:
|
||||||
|
"""restricted format: dict[str, dict[str, list<str>]]"""
|
||||||
|
if not dict_str_dict(d):
|
||||||
|
return False
|
||||||
|
for value in d.values():
|
||||||
|
for k, v in value.items():
|
||||||
|
if not isinstance(k, str) or not isinstance(v, list):
|
||||||
|
return False
|
||||||
|
if not all(isinstance(i, str) for i in v):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def bool_trans(d: dict) -> bool:
|
||||||
|
"""dict[str, dict[str, str] | dict[str, dict[str, str]] ]"""
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return False
|
||||||
|
if "data" not in d or "translate" not in d:
|
||||||
|
return False
|
||||||
|
if not dict_str_str(d["data"]):
|
||||||
|
return False
|
||||||
|
if not nested_3_dict_str_str(d["translate"]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
script_name = os.path.basename(__file__)
|
||||||
|
|
||||||
|
source_dir = "../miot/specs"
|
||||||
|
if not bool_trans(load_json(f"{source_dir}/bool_trans.json")):
|
||||||
|
print(script_name, "bool_trans FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/multi_lang.json")):
|
||||||
|
print(script_name, "multi_lang FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not spec_filter(load_json(f"{source_dir}/spec_filter.json")):
|
||||||
|
print(script_name, "spec_filter FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(
|
||||||
|
f"{source_dir}/std_ex_actions.json")):
|
||||||
|
print(script_name, "std_ex_actions.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(
|
||||||
|
f"{source_dir}/std_ex_devices.json")):
|
||||||
|
print(script_name, "std_ex_devices.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(f"{source_dir}/std_ex_events.json")):
|
||||||
|
print(script_name, "std_ex_events.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(
|
||||||
|
f"{source_dir}/std_ex_properties.json")):
|
||||||
|
print(script_name, "std_ex_properties.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(
|
||||||
|
f"{source_dir}/std_ex_services.json")):
|
||||||
|
print(script_name, "std_ex_services.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_2_dict_str_str(load_json(f"{source_dir}/std_ex_values.json")):
|
||||||
|
print(script_name, "std_ex_values.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
source_dir = "../miot/i18n"
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/de.json")):
|
||||||
|
print(script_name, "i18n de.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/en.json")):
|
||||||
|
print(script_name, "i18n en.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/es.json")):
|
||||||
|
print(script_name, "i18n es.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/fr.json")):
|
||||||
|
print(script_name, "i18n fr.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/ja.json")):
|
||||||
|
print(script_name, "i18n ja.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/ru.json")):
|
||||||
|
print(script_name, "i18n ru.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/zh-Hans.json")):
|
||||||
|
print(script_name, "i18n zh-Hans.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
if not nested_3_dict_str_str(load_json(f"{source_dir}/zh-Hant.json")):
|
||||||
|
print(script_name, "i18n zh-Hant.json FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(script_name, "PASS")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Get the script path.
|
||||||
|
script_path=$(dirname "$0")
|
||||||
|
# Change to the script path.
|
||||||
|
cd "$script_path"
|
||||||
|
# Set PYTHONPATH.
|
||||||
|
cd ..
|
||||||
|
export PYTHONPATH=`pwd`
|
||||||
|
echo "PYTHONPATH=$PYTHONPATH"
|
||||||
|
cd -
|
||||||
|
|
||||||
|
# Run the tests.
|
||||||
|
export source_dir="../miot/specs"
|
||||||
|
python3 json_format.py $source_dir/bool_trans.json
|
||||||
|
python3 json_format.py $source_dir/multi_lang.json
|
||||||
|
python3 json_format.py $source_dir/spec_filter.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_actions.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_devices.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_events.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_properties.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_services.json
|
||||||
|
python3 json_format.py $source_dir/std_ex_values.json
|
||||||
|
export source_dir="../miot/i18n"
|
||||||
|
python3 json_format.py $source_dir/de.json
|
||||||
|
python3 json_format.py $source_dir/en.json
|
||||||
|
python3 json_format.py $source_dir/es.json
|
||||||
|
python3 json_format.py $source_dir/fr.json
|
||||||
|
python3 json_format.py $source_dir/ja.json
|
||||||
|
python3 json_format.py $source_dir/ru.json
|
||||||
|
python3 json_format.py $source_dir/zh-Hans.json
|
||||||
|
python3 json_format.py $source_dir/zh-Hant.json
|
||||||
|
python3 rule_format.py
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Text entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.text import TextEntity
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_spec import MIoTSpecAction, MIoTSpecProperty
|
||||||
|
from .miot.miot_device import MIoTActionEntity, MIoTDevice, MIoTPropertyEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for prop in miot_device.prop_list.get('text', []):
|
||||||
|
new_entities.append(Text(miot_device=miot_device, spec=prop))
|
||||||
|
|
||||||
|
for action in miot_device.action_list.get('action_text', []):
|
||||||
|
new_entities.append(ActionText(
|
||||||
|
miot_device=miot_device, spec=action))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Text(MIoTPropertyEntity, TextEntity):
|
||||||
|
"""Text entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||||
|
"""Initialize the Text."""
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> Optional[str]:
|
||||||
|
"""Return the current text value."""
|
||||||
|
if isinstance(self._value, str):
|
||||||
|
return self._value[:255]
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
async def async_set_value(self, value: str) -> None:
|
||||||
|
"""Set the text value."""
|
||||||
|
await self.set_property_async(value=value)
|
||||||
|
|
||||||
|
|
||||||
|
class ActionText(MIoTActionEntity, TextEntity):
|
||||||
|
"""Text entities for Xiaomi Home."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
|
||||||
|
super().__init__(miot_device=miot_device, spec=spec)
|
||||||
|
self._attr_extra_state_attributes = {}
|
||||||
|
self._attr_native_value = ''
|
||||||
|
action_in: str = ', '.join([
|
||||||
|
f'{prop.description_trans}({prop.format_})'
|
||||||
|
for prop in self.spec.in_])
|
||||||
|
self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
|
||||||
|
# For action debug
|
||||||
|
self.action_platform = 'action_text'
|
||||||
|
|
||||||
|
async def async_set_value(self, value: str) -> None:
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
in_list: list = None
|
||||||
|
try:
|
||||||
|
in_list = json.loads(value)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
_LOGGER.error(
|
||||||
|
'action exec failed, %s(%s), invalid action params format, %s',
|
||||||
|
self.name, self.entity_id, value)
|
||||||
|
raise ValueError(
|
||||||
|
f'action exec failed, {self.name}({self.entity_id}), '
|
||||||
|
f'invalid action params format, {value}') from e
|
||||||
|
if not isinstance(in_list, list) or len(in_list) != len(self.spec.in_):
|
||||||
|
_LOGGER.error(
|
||||||
|
'action exec failed, %s(%s), invalid action params, %s',
|
||||||
|
self.name, self.entity_id, value)
|
||||||
|
raise ValueError(
|
||||||
|
f'action exec failed, {self.name}({self.entity_id}), '
|
||||||
|
f'invalid action params, {value}')
|
||||||
|
in_value: list[dict] = []
|
||||||
|
for index, prop in enumerate(self.spec.in_):
|
||||||
|
if type(in_list[index]).__name__ != prop.format_:
|
||||||
|
logging.error(
|
||||||
|
'action exec failed, %s(%s), invalid params item, which '
|
||||||
|
'item(%s) in the list must be %s, %s', self.name,
|
||||||
|
self.entity_id, prop.description_trans, prop.format_, value)
|
||||||
|
raise ValueError(
|
||||||
|
f'action exec failed, {self.name}({self.entity_id}), '
|
||||||
|
f'invalid params item, which item({prop.description_trans})'
|
||||||
|
f' in the list must be {prop.format_}, {value}')
|
||||||
|
in_value.append({'piid': prop.iid, 'value': in_list[index]})
|
||||||
|
|
||||||
|
self._attr_native_value = value
|
||||||
|
if await self.action_async(in_list=in_value):
|
||||||
|
self.async_write_ha_state()
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Xiaomi Home Integration",
|
||||||
|
"step": {
|
||||||
|
"eula": {
|
||||||
|
"title": "Risikohinweis",
|
||||||
|
"description": "1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\r\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\r\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet. \r\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\r\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.\r\n",
|
||||||
|
"data": {
|
||||||
|
"eula": "Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Grundkonfiguration",
|
||||||
|
"description": "### Anmeldegebiet\r\nWählen Sie das Gebiet, in dem sich Ihr Xiaomi Home-Konto befindet. Sie können es in der Xiaomi Home App unter `Mein (unten im Menü) > Weitere Einstellungen > Über Xiaomi Home` überprüfen.\r\n### Sprache\r\nWählen Sie die Sprache, in der Geräte und Entitätsnamen angezeigt werden. Teile von Sätzen, die nicht übersetzt sind, werden in Englisch angezeigt.\r\n### OAuth2-Authentifizierungs-Umleitungs-URL\r\nDie Umleitungs-URL für die OAuth2-Authentifizierung lautet **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant muss im selben lokalen Netzwerk wie das aktuelle Betriebsterminal (z. B. ein persönlicher Computer) und das Betriebsterminal muss über diese Adresse auf die Home Assistant-Homepage zugreifen können. Andernfalls kann die Anmeldeauthentifizierung fehlschlagen.\r\n### Hinweis\r\n- Für Benutzer mit Hunderten oder mehr Mi Home-Geräten wird das erste Hinzufügen der Integration einige Zeit in Anspruch nehmen. Bitte haben Sie Geduld.\r\n- Wenn Home Assistant in einer Docker-Umgebung läuft, stellen Sie bitte sicher, dass der Docker-Netzwerkmodus auf host eingestellt ist, da sonst die lokale Steuerungsfunktion möglicherweise nicht richtig funktioniert.\r\n- Die lokale Steuerungsfunktion der Integration hat einige Abhängigkeiten. Bitte lesen Sie das README sorgfältig.\r\n",
|
||||||
|
"data": {
|
||||||
|
"cloud_server": "Anmeldegebiet",
|
||||||
|
"integration_language": "Sprache",
|
||||||
|
"oauth_redirect_url": "OAuth2-Authentifizierungs-Umleitungs-URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "Fehler bei der Anmeldung",
|
||||||
|
"description": "Klicken Sie auf \"Weiter\", um es erneut zu versuchen."
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Familie und Geräte auswählen",
|
||||||
|
"description": "## Gebrauchsanweisung\r\n### Steuerungsmodus\r\n- Automatisch: Wenn im lokalen Netzwerk ein verfügbarer Xiaomi-Zentralgateway vorhanden ist, wird Home Assistant bevorzugt Steuerbefehle über den Zentralgateway senden, um eine lokale Steuerung zu ermöglichen. Wenn im lokalen Netzwerk kein Zentralgateway vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden, um eine lokale Steuerung zu ermöglichen. Nur wenn die oben genannten Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Steuerbefehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden nur über die Cloud gesendet.\r\n### Familienimport für importierte Geräte\r\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\r\n### Raumnamensynchronisationsmodus\r\nWenn Geräte von der Xiaomi Home App zu Home Assistant synchronisiert werden, wird die Bezeichnung des Bereichs, in dem sich die Geräte in Home Assistant befinden, nach folgenden Regeln benannt. Beachten Sie, dass das Synchronisieren von Geräten den von Xiaomi Home App festgelegten Familien- und Raum-Einstellungen nicht ändert.\r\n- Nicht synchronisieren: Das Gerät wird keinem Bereich hinzugefügt.\r\n- Andere Optionen: Der Bereich, in den das Gerät aufgenommen wird, wird nach dem Namen der Familie oder des Raums in der Xiaomi Home App benannt.\r\n### Action-Debug-Modus\r\nFür von MIoT-Spec-V2 definierte Gerätemethoden wird neben der Benachrichtigungs-Entität auch eine Texteingabe-Entität generiert. Damit können Sie bei der Fehlerbehebung Steuerbefehle an das Gerät senden.\r\n### Verstecke Nicht-Standard-Entitäten\r\nVerstecke Entitäten, die von nicht standardmäßigen MIoT-Spec-V2-Instanzen mit einem Namen beginnen, der mit einem \"*\" beginnt.\r\n\r\n \r\n### Hallo {nick_name}! Bitte wählen Sie den Steuerungsmodus der Integration sowie die Familie aus, in der sich die hinzuzufügenden Geräte befinden.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Steuerungsmodus",
|
||||||
|
"home_infos": "Familienimport für importierte Geräte",
|
||||||
|
"area_name_rule": "Raumnamensynchronisationsmodus",
|
||||||
|
"action_debug": "Action-Debug-Modus",
|
||||||
|
"hide_non_standard_entities": "Verstecke Nicht-Standard-Entitäten"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Klicken Sie hier, um sich anzumelden{link_right}\r\n(Sie werden nach erfolgreicher Anmeldung automatisch zur nächsten Seite weitergeleitet)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"eula_not_agree": "Bitte lesen Sie den Risikohinweis.",
|
||||||
|
"get_token_error": "Fehler beim Abrufen von Anmeldeinformationen (OAuth-Token).",
|
||||||
|
"get_homeinfo_error": "Fehler beim Abrufen von Familieninformationen.",
|
||||||
|
"mdns_discovery_error": "Lokaler Geräteerkennungsdienst ist nicht verfügbar.",
|
||||||
|
"get_cert_error": "Fehler beim Abrufen des Gateway-Zertifikats.",
|
||||||
|
"no_family_selected": "Keine Familie ausgewählt.",
|
||||||
|
"no_devices": "In der ausgewählten Familie sind keine Geräte enthalten. Bitte wählen Sie eine Familie mit Geräten aus und fahren Sie fort.",
|
||||||
|
"no_central_device": "Im Modus \"Xiaomi Central Hub Gateway\" muss ein verfügbares Xiaomi Central Hub Gateway im lokalen Netzwerk von Home Assistant vorhanden sein. Stellen Sie sicher, dass die ausgewählte Familie diese Anforderungen erfüllt."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "Konfiguration fehlgeschlagen. Netzwerkverbindung fehlgeschlagen. Überprüfen Sie die Netzwerkkonfiguration des Geräts.",
|
||||||
|
"already_configured": "Dieser Benutzer hat die Konfiguration bereits abgeschlossen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Konfiguration zu ändern.",
|
||||||
|
"invalid_auth_info": "Authentifizierungsinformationen sind abgelaufen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Authentifizierung erneut durchzuführen.",
|
||||||
|
"config_flow_error": "Integrationskonfigurationsfehler: {error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Authentifizierungskonfiguration",
|
||||||
|
"description": "Lokale Authentifizierungsinformationen sind abgelaufen. Starten Sie die Authentifizierung erneut.\r\n### Aktuelles Anmeldegebiet: {cloud_server}\r\n### OAuth2-Authentifizierungs-Umleitungs-URL\r\nDie Umleitungs-URL für die OAuth2-Authentifizierung lautet **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant muss im selben lokalen Netzwerk wie das aktuelle Betriebsterminal (z. B. ein persönlicher Computer) und das Betriebsterminal muss über diese Adresse auf die Home Assistant-Homepage zugreifen können. Andernfalls kann die Anmeldeauthentifizierung fehlschlagen.\r\n",
|
||||||
|
"data": {
|
||||||
|
"oauth_redirect_url": "OAuth2-Authentifizierungs-Umleitungs-URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "Fehler bei der Anmeldung",
|
||||||
|
"description": "Klicken Sie auf \"Weiter\", um es erneut zu versuchen."
|
||||||
|
},
|
||||||
|
"config_options": {
|
||||||
|
"title": "Konfigurationsoptionen",
|
||||||
|
"description": "### Hallo {nick_name}!\r\n\r\nXiaomi Home-Konto-ID: {uid}\r\nAktuelles Anmeldegebiet: {cloud_server}\r\n\r\nWählen Sie die Optionen aus, die Sie erneut konfigurieren möchten, und klicken Sie dann auf \"Weiter\".\r\n",
|
||||||
|
"data": {
|
||||||
|
"integration_language": "Integrationsprache",
|
||||||
|
"update_user_info": "Benutzerinformationen aktualisieren",
|
||||||
|
"update_devices": "Geräteliste aktualisieren",
|
||||||
|
"action_debug": "Action-Debug-Modus",
|
||||||
|
"hide_non_standard_entities": "Verstecke Nicht-Standard-Entitäten",
|
||||||
|
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren (globale konfiguration)",
|
||||||
|
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren (globale Konfiguration)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_user_info": {
|
||||||
|
"title": "Benutzernamen aktualisieren",
|
||||||
|
"description": "{nick_name}! Bitte geben Sie unten Ihren Benutzernamen ein.\r\n",
|
||||||
|
"data": {
|
||||||
|
"nick_name": "Benutzername"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Familie und Geräte neu auswählen",
|
||||||
|
"description": "## Gebrauchsanweisung\r\n### Steuerungsmodus\r\n- Automatisch: Wenn im lokalen Netzwerk ein verfügbarer Xiaomi-Zentralgateway vorhanden ist, wird Home Assistant bevorzugt Steuerbefehle über den Zentralgateway senden, um eine lokale Steuerung zu ermöglichen. Wenn im lokalen Netzwerk kein Zentralgateway vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden, um eine lokale Steuerung zu ermöglichen. Nur wenn die oben genannten Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Steuerbefehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden nur über die Cloud gesendet.\r\n### Familienimport für importierte Geräte\r\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\r\n \r\n### Hallo {nick_name}! Bitte wählen Sie den Steuerungsmodus der Integration sowie die Familie aus, in der sich die hinzuzufügenden Geräte befinden.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Steuerungsmodus",
|
||||||
|
"home_infos": "Familienimport für importierte Geräte"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_trans_rules": {
|
||||||
|
"title": "Entitätskonvertierungsregeln aktualisieren",
|
||||||
|
"description": "## Gebrauchsanweisung\r\n- Aktualisieren Sie die Entitätsinformationen der Geräte im aktuellen Integrationsinstanz, einschließlich der mehrsprachigen SPEC-Konfiguration, der SPEC-Booleschen Übersetzung und der SPEC-Modellfilterung.\r\n- **Warnung: Diese Konfiguration ist eine globale Konfiguration** und aktualisiert direkt den lokalen Cache. Wenn in anderen Integrationsinstanzen Geräte desselben Modells vorhanden sind, werden diese nach dem Neuladen der entsprechenden Instanzen ebenfalls aktualisiert.\r\n- Dieser Vorgang kann einige Zeit in Anspruch nehmen, bitte haben Sie Geduld. Wählen Sie \"Bestätigen Sie das Update\" und klicken Sie auf \"Weiter\", um **{urn_count}** Regeln zu aktualisieren, andernfalls überspringen Sie das Update.\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Bestätigen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_lan_ctrl_config": {
|
||||||
|
"title": "LAN-Steuerungskonfiguration aktualisieren",
|
||||||
|
"description": "## Gebrauchsanweisung\r\nAktualisieren Sie die Konfigurationsinformationen für **LAN-Steuerung von Xiaomi Home-Geräten**. Wenn die Cloud und das zentrale Gateway die Geräte nicht steuern können, versucht die Integration, die Geräte über das LAN zu steuern; wenn keine Netzwerkkarte ausgewählt ist, wird die LAN-Steuerung nicht aktiviert.\r\n- Derzeit werden nur **SPEC v2** WiFi-Geräte im LAN unterstützt. Einige ältere Geräte unterstützen möglicherweise keine Steuerung oder Eigenschaftssynchronisierung.\r\n- Bitte wählen Sie die Netzwerkkarte(n) im selben Netzwerk wie die Geräte aus (mehrere Auswahlen werden unterstützt). Wenn die ausgewählte Netzwerkkarte zwei oder mehr Verbindungen im selben Netzwerk hat, wird empfohlen, die mit der besten Netzwerkverbindung auszuwählen, da sonst die **normale Verwendung der Geräte beeinträchtigt werden kann**.\r\n- **Wenn es im LAN Endgeräte (Gateways, Mobiltelefone usw.) gibt, die lokale Steuerung unterstützen, kann das Aktivieren des LAN-Abonnements lokale Automatisierung oder Geräteanomalien verursachen. Bitte verwenden Sie es mit Vorsicht**.\r\n- **Warnung: Diese Konfiguration ist global und Änderungen wirken sich auf andere Integrationsinstanzen aus. Bitte ändern Sie sie mit Vorsicht**.\r\n{notice_net_dup}\r\n",
|
||||||
|
"data": {
|
||||||
|
"net_interfaces": "Bitte wählen Sie die zu verwendende Netzwerkkarte aus",
|
||||||
|
"enable_subscribe": "LAN-Abonnement aktivieren"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_confirm": {
|
||||||
|
"title": "Bestätigen Sie die Konfiguration",
|
||||||
|
"description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Änderungen bestätigen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Klicken Sie hier, um sich erneut anzumelden{link_right}"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"not_auth": "Nicht authentifiziert. Klicken Sie auf den Authentifizierungslink, um die Benutzeridentität zu authentifizieren.",
|
||||||
|
"get_token_error": "Fehler beim Abrufen von Anmeldeinformationen (OAuth-Token).",
|
||||||
|
"get_homeinfo_error": "Fehler beim Abrufen von Home-Informationen.",
|
||||||
|
"get_cert_error": "Fehler beim Abrufen des Zentralzertifikats.",
|
||||||
|
"no_family_selected": "Keine Familie ausgewählt.",
|
||||||
|
"no_devices": "In der ausgewählten Familie sind keine Geräte vorhanden. Bitte wählen Sie eine Familie mit Geräten und fahren Sie dann fort.",
|
||||||
|
"no_central_device": "Der Modus \"Zentral Gateway\" erfordert ein verfügbares Xiaomi-Zentral-Gateway im lokalen Netzwerk, in dem Home Assistant installiert ist. Überprüfen Sie, ob die ausgewählte Familie diese Anforderung erfüllt.",
|
||||||
|
"mdns_discovery_error": "Lokaler Geräteerkennungsdienstfehler.",
|
||||||
|
"update_config_error": "Fehler beim Aktualisieren der Konfigurationsinformationen.",
|
||||||
|
"not_confirm": "Änderungen wurden nicht bestätigt. Bitte bestätigen Sie die Auswahl, bevor Sie sie einreichen."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "Konfiguration fehlgeschlagen. Netzwerkverbindungsfehler. Überprüfen Sie die Netzwerkkonfiguration des Geräts.",
|
||||||
|
"options_flow_error": "Integrationsneukonfigurationsfehler: {error}",
|
||||||
|
"re_add": "Fügen Sie die Integration erneut hinzu. Fehlermeldung: {error}",
|
||||||
|
"storage_error": "Integrations-Speichermodulfehler. Bitte versuchen Sie es erneut oder fügen Sie die Integration erneut hinzu: {error}",
|
||||||
|
"inconsistent_account": "Kontoinformationen sind inkonsistent. Bitte melden Sie sich mit den richtigen Kontoinformationen an."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Xiaomi Home Integration",
|
||||||
|
"step": {
|
||||||
|
"eula": {
|
||||||
|
"title": "Risk Notice",
|
||||||
|
"description": "1. Your Xiaomi user information and device information will be stored in the Home Assistant system. **Xiaomi cannot guarantee the security of the Home Assistant storage mechanism**. You are responsible for preventing your information from being stolen.\r\n2. This integration is maintained by the open-source community. There may be stability issues or other problems. When encountering issues or bugs of this integration, **you should seek help from the open-source community rather than contacting Xiaomi customer service**.\r\n3. You need some technical ability to maintain your local operating environment. The integration is not user-friendly for beginners.\r\n4. Please read the README file before starting.\n\n5. To ensure stable use of the integration and prevent interface abuse, **this integration is only allowed to be used in Home Assistant. For details, please refer to the LICENSE**.\r\n",
|
||||||
|
"data": {
|
||||||
|
"eula": "I am aware of the above risks and willing to voluntarily assume any risks associated with the use of the integration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Basic configuration",
|
||||||
|
"description": "### Login Region\r\nSelect the region of your Xiaomi account. You can find it in the Xiaomi Home APP > Profile (located in the menu at the bottom) > Additional settings > About Xiaomi Home.\r\n### Language\r\nSelect the language of the device and entity names. Some sentences without translation will be displayed in English.\r\n### OAuth2 Redirect URL\r\nThe OAuth2 authentication redirect address is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. The Home Assistant needs to be in the same local area network as the current operating terminal (e.g., the personal computer) and the operating terminal can access the Home Assistant home page through this address. Otherwise, the login authentication may fail.\r\n### Note\r\n- For users with hundreds or more Mi Home devices, the initial addition of the integration will take some time. Please be patient.\r\n- If Home Assistant is running in a Docker environment, please ensure that the Docker network mode is set to host, otherwise local control functionality may not work properly.\r\n- The local control functionality of the integration has some dependencies. Please read the README carefully.\r\n",
|
||||||
|
"data": {
|
||||||
|
"cloud_server": "Login Region",
|
||||||
|
"integration_language": "Language",
|
||||||
|
"oauth_redirect_url": "OAuth2 Redirect URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "Login Error",
|
||||||
|
"description": "Click NEXT to try again."
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Select Home and Devices",
|
||||||
|
"description": "## Usage Instructions\r\n### Control mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi LAN control function. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Import devices from home\r\nThe integration will add devices from the selected homes.\n### Room name synchronizing mode\nWhen importing devices from Xiaomi Home APP to Home Assistant, the naming convention of the area where the device is added to is as follows. Note that the device synchronizing process does not change the home or room settings in Xiaomi Home APP.\r\n- Do not synchronize: The device will not be added to any area.\r\n- Other options: The device will be added to an area named as the home and/or room name that already exists in Xiaomi Home APP.\r\n### Debug mode for action\r\nFor the action defined in MIoT-Spec-V2 of the device, a Text entity along with a Notify entity will be created, in which you can send control commands to the device for debugging.\r\n### Hide non-standard created entities\r\nHide the entities generated from non-standard MIoT-Spec-V2 instances, whose names begin with \"*\".\r\n\r\n \r\n### Hello {nick_name}, please select the integration control mode and the home where the device you want to import.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Control mode",
|
||||||
|
"home_infos": "Import devices from home",
|
||||||
|
"area_name_rule": "Room name synchronizing mode",
|
||||||
|
"action_debug": "Debug mode for action",
|
||||||
|
"hide_non_standard_entities": "Hide non-standard created entities"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Click here to login{link_right}\r\n(You will be automatically redirected to the next page after a successful login)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"eula_not_agree": "Please read the risk notice.",
|
||||||
|
"get_token_error": "Failed to retrieve login authorization information (OAuth token).",
|
||||||
|
"get_homeinfo_error": "Failed to retrieve home information.",
|
||||||
|
"mdns_discovery_error": "Local device discovery service exception.",
|
||||||
|
"get_cert_error": "Failed to retrieve the central hub gateway certificate.",
|
||||||
|
"no_family_selected": "No home selected.",
|
||||||
|
"no_devices": "The selected home does not have any devices. Please choose a home containing devices and continue.",
|
||||||
|
"no_central_device": "[Central Hub Gateway Mode] requires a Xiaomi central hub gateway available in the local network where Home Assistant exists. Please check if the selected home meets the requirement."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "Configuration failed. The network connection is abnormal. Please check the equipment network configuration.",
|
||||||
|
"already_configured": "Configuration for this user is already completed. Please go to the integration page and click the CONFIGURE button for modifications.",
|
||||||
|
"invalid_auth_info": "Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.",
|
||||||
|
"config_flow_error": "Integration configuration error: {error}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Authentication Configuration",
|
||||||
|
"description": "Local authentication information has expired. Please restart the authentication process.\r\n### Current Login Region: {cloud_server}\r\n### OAuth2 Redirect URL\r\nThe OAuth2 authentication redirect address is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. The Home Assistant needs to be in the same local area network as the current operating terminal (e.g., the personal computer) and the operating terminal can access the Home Assistant home page through this address. Otherwise, the login authentication may fail.\r\n",
|
||||||
|
"data": {
|
||||||
|
"oauth_redirect_url": "OAuth2 Redirect URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "An error occurred during login.",
|
||||||
|
"description": "Click NEXT to retry."
|
||||||
|
},
|
||||||
|
"config_options": {
|
||||||
|
"title": "Configuration Options",
|
||||||
|
"description": "### Hello, {nick_name}\r\n\r\nXiaomi ID: {uid}\r\nCurrent Login Region: {cloud_server}\r\n\r\nPlease select the options you need to configure, then click NEXT.\r\n",
|
||||||
|
"data": {
|
||||||
|
"integration_language": "Integration Language",
|
||||||
|
"update_user_info": "Update user information",
|
||||||
|
"update_devices": "Update device list",
|
||||||
|
"action_debug": "Debug mode for action",
|
||||||
|
"hide_non_standard_entities": "Hide non-standard created entities",
|
||||||
|
"update_trans_rules": "Update entity conversion rules",
|
||||||
|
"update_lan_ctrl_config": "Update LAN control configuration"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_user_info": {
|
||||||
|
"title": "Update User Nickname",
|
||||||
|
"description": "Hello {nick_name}, you can modify your custom nickname below.\r\n",
|
||||||
|
"data": {
|
||||||
|
"nick_name": "Nick Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Re-select Home and Devices",
|
||||||
|
"description": "## Usage Instructions\r\n### Control mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi LAN control function. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Import devices from home\r\nThe integration will add devices from the selected homes.\r\n \r\n### Hello {nick_name}, please select the integration control mode and the home where the device you want to import.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Control mode",
|
||||||
|
"home_infos": "Import devices from home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_trans_rules": {
|
||||||
|
"title": "Update Entities Transformation Rules",
|
||||||
|
"description": "## Usage Instructions\r\n- Update the entity information of devices in the current integration instance, including MIoT-Spec-V2 multilingual configuration, boolean translation, and model filtering.\r\n- **Warning**: This is a global configuration and will update the local cache. It will affect all integration instances.\r\n- This operation will take some time, please be patient. Check \"Confirm Update\" and click \"Next\" to start updating **{urn_count}** rules, otherwise skip the update.\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Confirm the update"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_lan_ctrl_config": {
|
||||||
|
"title": "Update lan control configuration",
|
||||||
|
"description": "## Usage Instructions\r\nUpdate the configurations for Xiaomi LAN control function. When the cloud and the central hub gateway cannot control the devices, the integration will attempt to control the devices through the LAN. If no network card is selected, the LAN control function will not take effect.\r\n- Only MIoT-Spec-V2 compatible IP devices in the LAN are supported. Some devices produced before 2020 may not support LAN control or LAN subscription.\r\n- Please select the network card(s) on the same network as the devices to be controlled. Multiple network cards can be selected. If Home Assistant have two or more connections to the local area network because of the multiple selection of the network cards, it is recommended to select the one with the best network connection, otherwise it may have bad effect on the devices.\r\n- If there are terminal devices (Xiaomi speaker with screen, mobile phone, etc.) in the LAN that support local control, enabling LAN subscription may cause local automation and device anomalies.\r\n- **Warning**: This is a global configuration. It will affect all integration instances. Please use it with caution.\r\n{notice_net_dup}\r\n",
|
||||||
|
"data": {
|
||||||
|
"net_interfaces": "Please select the network card to use",
|
||||||
|
"enable_subscribe": "Enable LAN subscription"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_confirm": {
|
||||||
|
"title": "Confirm Configuration",
|
||||||
|
"description": "Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\r\nThe integration will reload using the updated configuration.\r\n\r\nIntegration Language: \t{lang_new}\r\nNickname: \t{nick_name_new}\r\nDebug mode for action: \t{action_debug}\r\nHide non-standard created entities: \t{hide_non_standard_entities}\r\nDevice Changes: \tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\r\nTransformation rules change: \tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Confirm the change"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Please click here to re-login{link_right}"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"not_auth": "Not authenticated. Please click the authentication link to authenticate user identity.",
|
||||||
|
"get_token_error": "Failed to retrieve login authorization information (OAuth token).",
|
||||||
|
"get_homeinfo_error": "Failed to retrieve home information.",
|
||||||
|
"get_cert_error": "Failed to retrieve the central hub gateway certificate.",
|
||||||
|
"no_devices": "The selected home does not have any devices. Please choose a home containing devices and continue.",
|
||||||
|
"no_family_selected": "No home selected.",
|
||||||
|
"no_central_device": "[Central Hub Gateway Mode] requires a Xiaomi central hub gateway available in the local network where Home Assistant exists. Please check if the selected home meets the requirement.",
|
||||||
|
"mdns_discovery_error": "Local device discovery service exception.",
|
||||||
|
"update_config_error": "Failed to update configuration information.",
|
||||||
|
"not_confirm": "Changes are not confirmed. Please confirm the change before submitting."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "Configuration failed. The network connection is abnormal. Please check the equipment network configuration.",
|
||||||
|
"options_flow_error": "Integration re-configuration error: {error}",
|
||||||
|
"re_add": "Please re-add the integration. Error message: {error}",
|
||||||
|
"storage_error": "Integration storage module exception. Please try again or re-add the integration: {error}",
|
||||||
|
"inconsistent_account": "Account information is inconsistent. Please login with the correct account."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Integración de Xiaomi Home",
|
||||||
|
"step": {
|
||||||
|
"eula": {
|
||||||
|
"title": "Aviso de riesgo",
|
||||||
|
"description": "1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\r\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\r\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\r\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**. \r\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.\r\n",
|
||||||
|
"data": {
|
||||||
|
"eula": "He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Configuración básica",
|
||||||
|
"description": "### Región de inicio de sesión\r\nSeleccione la región donde se encuentra su cuenta de Xiaomi. Puede consultar esta información en `Xiaomi Home APP > Yo (ubicado en el menú inferior) > Más ajustes > Acerca de Xiaomi Home`.\r\n### Idioma\r\nSeleccione el idioma utilizado para los nombres de los dispositivos y entidades. Las partes de las frases que no están traducidas se mostrarán en inglés.\r\n### Dirección de redireccionamiento de autenticación de OAuth2\r\nLa dirección de redireccionamiento de autenticación de OAuth2 es **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant debe estar en la misma red local que el terminal de operación actual (por ejemplo, una computadora personal) y el terminal de operación debe poder acceder a la página de inicio de Home Assistant a través de esta dirección, de lo contrario, la autenticación de inicio de sesión podría fallar.\r\n### Nota\r\n- Para los usuarios con cientos o más dispositivos Mi Home, la adición inicial de la integración tomará algún tiempo. Por favor, sea paciente.\r\n- Si Home Assistant se está ejecutando en un entorno Docker, asegúrese de que el modo de red de Docker esté configurado en host, de lo contrario, la funcionalidad de control local puede no funcionar correctamente.\r\n- La funcionalidad de control local de la integración tiene algunas dependencias. Por favor, lea el README cuidadosamente.\r\n",
|
||||||
|
"data": {
|
||||||
|
"cloud_server": "Región de inicio de sesión",
|
||||||
|
"integration_language": "Idioma",
|
||||||
|
"oauth_redirect_url": "Dirección de redireccionamiento de autenticación de OAuth2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "Error de inicio de sesión",
|
||||||
|
"description": "Haga clic en \"Siguiente\" para volver a intentarlo"
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Seleccionar hogares y dispositivos",
|
||||||
|
"description": "## Instrucciones de uso\r\n### Modo de control\r\n- Automático: Cuando hay un gateway central de Xiaomi disponible en la red local, Home Assistant priorizará el envío de comandos de control de dispositivos a través del gateway central para lograr un control localizado. Si no hay un gateway central en la red local, intentará enviar comandos de control a través del protocolo Xiaomi OT para lograr un control localizado. Solo cuando no se cumplan las condiciones anteriores de control localizado, los comandos de control del dispositivo se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Hogares de dispositivos importados\r\nLa integración agregará los dispositivos en los hogares seleccionados.\r\n### Modo de sincronización del nombre de la habitación\r\nCuando se sincronizan los dispositivos desde la aplicación Xiaomi Home a Home Assistant, los nombres de las áreas donde se encuentran los dispositivos en Home Assistant seguirán las reglas de nomenclatura a continuación. Tenga en cuenta que el proceso de sincronización de dispositivos no cambiará la configuración de hogares y habitaciones en la aplicación Xiaomi Home.\r\n- Sin sincronización: el dispositivo no se agregará a ninguna área.\r\n- Otras opciones: la zona donde se agrega el dispositivo tendrá el mismo nombre que el hogar o la habitación en la aplicación Xiaomi Home.\r\n### Modo de depuración de Action\r\nPara los métodos definidos por MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro de entrada de texto que se puede utilizar para enviar comandos de control al dispositivo durante la depuración.\r\n### Ocultar entidades generadas no estándar\r\nOcultar las entidades generadas por la instancia no estándar MIoT-Spec-V2 que comienzan con \"*\".\r\n\r\n \r\n### ¡Hola, {nick_name}! Seleccione el modo de control de integración y el hogar donde se encuentran los dispositivos que desea agregar.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Modo de control",
|
||||||
|
"home_infos": "Hogares de dispositivos importados",
|
||||||
|
"area_name_rule": "Modo de sincronización del nombre de la habitación",
|
||||||
|
"action_debug": "Modo de depuración de Action",
|
||||||
|
"hide_non_standard_entities": "Ocultar entidades generadas no estándar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Haga clic aquí para iniciar sesión de nuevo{link_right}\r\n(Será redirigido automáticamente a la siguiente página después de un inicio de sesión exitoso)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"eula_not_agree": "Lea el texto de aviso de riesgo.",
|
||||||
|
"get_token_error": "Error al obtener la información de autorización de inicio de sesión (token OAuth).",
|
||||||
|
"get_homeinfo_error": "Error al obtener la información del hogar.",
|
||||||
|
"mdns_discovery_error": "Error en el servicio de descubrimiento de dispositivos locales.",
|
||||||
|
"get_cert_error": "Error al obtener el certificado de la puerta de enlace.",
|
||||||
|
"no_family_selected": "No se ha seleccionado ningún hogar.",
|
||||||
|
"no_devices": "No hay dispositivos en el hogar seleccionado. Seleccione un hogar con dispositivos y continúe.",
|
||||||
|
"no_central_device": "【Modo de puerta de enlace central】Se requiere una puerta de enlace Xiaomi disponible en la red local donde se encuentra Home Assistant. Verifique si el hogar seleccionado cumple con este requisito."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.",
|
||||||
|
"already_configured": "Esta cuenta ya ha finalizado la configuración. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para modificar la configuración.",
|
||||||
|
"invalid_auth_info": "La información de autorización ha caducado. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para volver a autenticarse.",
|
||||||
|
"config_flow_error": "Error de configuración de integración: {error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"auth_config": {
|
||||||
|
"title": "Configuración de autorización",
|
||||||
|
"description": "Se detectó que la información de autenticación local ha caducado, vuelva a autenticarse\r\n### Región de inicio de sesión actual: {cloud_server}\r\n### Dirección de redireccionamiento de autenticación de OAuth2\r\nLa dirección de redireccionamiento de autenticación de OAuth2 es **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant debe estar en la misma red local que el terminal de operación actual (por ejemplo, una computadora personal) y el terminal de operación debe poder acceder a la página de inicio de Home Assistant a través de esta dirección, de lo contrario, la autenticación de inicio de sesión podría fallar.\r\n",
|
||||||
|
"data": {
|
||||||
|
"oauth_redirect_url": "Dirección de redireccionamiento de autenticación de OAuth2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_error": {
|
||||||
|
"title": "Error de inicio de sesión",
|
||||||
|
"description": "Haga clic en \"Siguiente\" para volver a intentarlo\r\n"
|
||||||
|
},
|
||||||
|
"config_options": {
|
||||||
|
"title": "Opciones de configuración",
|
||||||
|
"description": "### ¡Hola, {nick_name}!\r\n\r\nID de cuenta de Xiaomi: {uid}\r\nRegión de inicio de sesión actual: {cloud_server}\r\n\r\nSeleccione las opciones que desea reconfigurar y haga clic en \"Siguiente\".\r\n",
|
||||||
|
"data": {
|
||||||
|
"integration_language": "Idioma de la integración",
|
||||||
|
"update_user_info": "Actualizar información de usuario",
|
||||||
|
"update_devices": "Actualizar lista de dispositivos",
|
||||||
|
"action_debug": "Modo de depuración de Action",
|
||||||
|
"hide_non_standard_entities": "Ocultar entidades generadas no estándar",
|
||||||
|
"update_trans_rules": "Actualizar reglas de conversión de entidad (configuración global)",
|
||||||
|
"update_lan_ctrl_config": "Actualizar configuración de control LAN (configuración global)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_user_info": {
|
||||||
|
"title": "Actualizar apodo de usuario",
|
||||||
|
"description": "¡Hola, {nick_name}! Modifique su apodo de usuario a continuación.\r\n",
|
||||||
|
"data": {
|
||||||
|
"nick_name": "Apodo de usuario"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devices_filter": {
|
||||||
|
"title": "Recomendar hogares y dispositivos",
|
||||||
|
"description": "## Instrucciones de uso\r\n### Modo de control\r\n- Automático: Cuando hay un gateway central de Xiaomi disponible en la red local, Home Assistant priorizará el envío de comandos de control de dispositivos a través del gateway central para lograr un control localizado. Si no hay un gateway central en la red local, intentará enviar comandos de control a través del protocolo Xiaomi OT para lograr un control localizado. Solo cuando no se cumplan las condiciones anteriores de control localizado, los comandos de control del dispositivo se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Hogares de dispositivos importados\r\nLa integración agregará los dispositivos en los hogares seleccionados.\r\n \r\n### ¡Hola, {nick_name}! Seleccione el modo de control de integración y el hogar donde se encuentran los dispositivos que desea agregar.\r\n",
|
||||||
|
"data": {
|
||||||
|
"ctrl_mode": "Modo de control",
|
||||||
|
"home_infos": "Hogares de dispositivos importados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_trans_rules": {
|
||||||
|
"title": "Actualizar reglas de conversión de entidad",
|
||||||
|
"description": "## Instrucciones de uso\r\n- Actualice la información de la entidad de los dispositivos en la instancia de integración actual, incluida la configuración multilingüe de SPEC, la traducción booleana de SPEC y el filtrado de modelos de SPEC.\r\n- **Advertencia: Esta configuración es una configuración global** y actualizará directamente la caché local. Si hay dispositivos del mismo modelo en otras instancias de integración, las instancias relevantes también se actualizarán después de recargarlas.\r\n- Esta operación tomará algún tiempo, por favor sea paciente. Marque \"Confirmar actualización\" y haga clic en \"Siguiente\" para comenzar a actualizar **{urn_count}** reglas, de lo contrario, omita la actualización.\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Confirmar actualización"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update_lan_ctrl_config": {
|
||||||
|
"title": "Actualizar configuración de control LAN",
|
||||||
|
"description": "## Instrucciones de uso\r\nActualice la información de configuración para **control LAN de dispositivos Xiaomi Home**. Cuando la nube y la puerta de enlace central no puedan controlar los dispositivos, la integración intentará controlar los dispositivos a través de la LAN; si no se selecciona ninguna tarjeta de red, el control LAN no se habilitará.\r\n- Actualmente, solo se admiten dispositivos WiFi **SPEC v2** en la LAN. Algunos dispositivos más antiguos pueden no admitir el control o la sincronización de propiedades.\r\n- Seleccione la(s) tarjeta(s) de red en la misma red que los dispositivos (se admiten múltiples selecciones). Si la tarjeta de red seleccionada tiene dos o más conexiones en la misma red, se recomienda seleccionar la que tenga la mejor conexión de red, de lo contrario, puede **afectar el uso normal de los dispositivos**.\r\n- **Si hay dispositivos terminales (puertas de enlace, teléfonos móviles, etc.) en la LAN que admiten el control local, habilitar la suscripción LAN puede causar automatización local o anomalías en los dispositivos. Úselo con precaución**.\r\n- **Advertencia: Esta configuración es global y los cambios afectarán a otras instancias de integración. Modifique con precaución**.\r\n{notice_net_dup}\r\n",
|
||||||
|
"data": {
|
||||||
|
"net_interfaces": "Por favor, seleccione la tarjeta de red a utilizar",
|
||||||
|
"enable_subscribe": "Habilitar suscripción LAN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_confirm": {
|
||||||
|
"title": "Confirmar configuración",
|
||||||
|
"description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas\r\n",
|
||||||
|
"data": {
|
||||||
|
"confirm": "Confirmar modificación"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"oauth": "### {link_left}Haga clic aquí para iniciar sesión de nuevo{link_right}"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"not_auth": "Usuario no autenticado. Haga clic en el enlace de autenticación para autenticarse.",
|
||||||
|
"get_token_error": "Error al obtener la información de autorización de inicio de sesión (token OAuth).",
|
||||||
|
"get_homeinfo_error": "Error al obtener la información del hogar.",
|
||||||
|
"get_cert_error": "Error al obtener el certificado de la puerta de enlace.",
|
||||||
|
"no_family_selected": "No se ha seleccionado ningún hogar.",
|
||||||
|
"no_devices": "No hay dispositivos en el hogar seleccionado. Seleccione un hogar con dispositivos y continúe.",
|
||||||
|
"no_central_device": "【Modo de puerta de enlace central】Se requiere una puerta de enlace Xiaomi disponible en la red local donde se encuentra Home Assistant. Verifique si el hogar seleccionado cumple con este requisito.",
|
||||||
|
"mdns_discovery_error": "Error en el servicio de descubrimiento de dispositivos locales.",
|
||||||
|
"update_config_error": "Error al actualizar la información de configuración.",
|
||||||
|
"not_confirm": "No se ha confirmado la opción de modificación. Seleccione y confirme la opción antes de enviar."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"network_connect_error": "La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.",
|
||||||
|
"options_flow_error": "Error al reconfigurar la integración: {error}",
|
||||||
|
"re_add": "Agregue la integración de nuevo, mensaje de error: {error}",
|
||||||
|
"storage_error": "Error en el módulo de almacenamiento de integración. Intente de nuevo o agregue la integración de nuevo: {error}",
|
||||||
|
"inconsistent_account": "La información de la cuenta no coincide. Inicie sesión con la cuenta correcta."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Vacuum entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.vacuum import (
|
||||||
|
StateVacuumEntity,
|
||||||
|
VacuumEntityFeature
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
||||||
|
from .miot.miot_spec import (
|
||||||
|
MIoTSpecAction,
|
||||||
|
MIoTSpecProperty)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('vacuum', []):
|
||||||
|
new_entities.append(
|
||||||
|
Vacuum(miot_device=miot_device, entity_data=data))
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
||||||
|
"""Vacuum entities for Xiaomi Home."""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
_prop_status: Optional[MIoTSpecProperty]
|
||||||
|
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||||
|
_prop_battery_level: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
_action_start_sweep: Optional[MIoTSpecAction]
|
||||||
|
_action_stop_sweeping: Optional[MIoTSpecAction]
|
||||||
|
_action_pause_sweeping: Optional[MIoTSpecAction]
|
||||||
|
_action_continue_sweep: Optional[MIoTSpecAction]
|
||||||
|
_action_stop_and_gocharge: Optional[MIoTSpecAction]
|
||||||
|
_action_identify: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
_status_map: Optional[dict[int, str]]
|
||||||
|
_fan_level_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_supported_features = VacuumEntityFeature(0)
|
||||||
|
|
||||||
|
self._prop_status = None
|
||||||
|
self._prop_fan_level = None
|
||||||
|
self._prop_battery_level = None
|
||||||
|
self._action_start_sweep = None
|
||||||
|
self._action_stop_sweeping = None
|
||||||
|
self._action_pause_sweeping = None
|
||||||
|
self._action_continue_sweep = None
|
||||||
|
self._action_stop_and_gocharge = None
|
||||||
|
self._action_identify = None
|
||||||
|
self._status_map = None
|
||||||
|
self._fan_level_map = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'status':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid status value_list, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._status_map = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._prop_status = prop
|
||||||
|
elif prop.name == 'fan-level':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid fan-level value_list, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._fan_level_map = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_fan_speed_list = list(self._fan_level_map.values())
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
||||||
|
self._prop_fan_level = prop
|
||||||
|
|
||||||
|
elif prop.name == 'battery-level':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.BATTERY
|
||||||
|
self._prop_battery_level = prop
|
||||||
|
# action
|
||||||
|
for action in entity_data.actions:
|
||||||
|
if action.name == 'start-sweep':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.START
|
||||||
|
self._action_start_sweep = action
|
||||||
|
elif action.name == 'stop-sweeping':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.STOP
|
||||||
|
self._action_stop_sweeping = action
|
||||||
|
elif action.name == 'pause-sweeping':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.PAUSE
|
||||||
|
self._action_pause_sweeping = action
|
||||||
|
elif action.name == 'continue-sweep':
|
||||||
|
self._action_continue_sweep = action
|
||||||
|
elif action.name == 'stop-and-gocharge':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||||
|
self._action_stop_and_gocharge = action
|
||||||
|
|
||||||
|
elif action.name == 'identify':
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.LOCATE
|
||||||
|
self._action_identify = action
|
||||||
|
|
||||||
|
async def async_start(self) -> None:
|
||||||
|
"""Start or resume the cleaning task."""
|
||||||
|
if self.state.lower() in ['paused', '暂停中']:
|
||||||
|
await self.action_async(action=self._action_continue_sweep)
|
||||||
|
return
|
||||||
|
await self.action_async(action=self._action_start_sweep)
|
||||||
|
|
||||||
|
async def async_stop(self, **kwargs: Any) -> None:
|
||||||
|
"""Stop the vacuum cleaner, do not return to base."""
|
||||||
|
await self.action_async(action=self._action_stop_sweeping)
|
||||||
|
|
||||||
|
async def async_pause(self) -> None:
|
||||||
|
"""Pause the cleaning task."""
|
||||||
|
await self.action_async(action=self._action_pause_sweeping)
|
||||||
|
|
||||||
|
async def async_return_to_base(self, **kwargs: Any) -> None:
|
||||||
|
"""Set the vacuum cleaner to return to the dock."""
|
||||||
|
await self.action_async(action=self._action_stop_and_gocharge)
|
||||||
|
|
||||||
|
async def async_clean_spot(self, **kwargs: Any) -> None:
|
||||||
|
"""Perform a spot clean-up."""
|
||||||
|
|
||||||
|
async def async_locate(self, **kwargs: Any) -> None:
|
||||||
|
"""Locate the vacuum cleaner."""
|
||||||
|
await self.action_async(action=self._action_identify)
|
||||||
|
|
||||||
|
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||||
|
"""Set fan speed."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[str]:
|
||||||
|
"""Return the current state of the vacuum cleaner."""
|
||||||
|
return self.get_map_description(
|
||||||
|
map_=self._status_map,
|
||||||
|
key=self.get_prop_value(prop=self._prop_status))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self) -> Optional[int]:
|
||||||
|
"""Return the current battery level of the vacuum cleaner."""
|
||||||
|
return self.get_prop_value(prop=self._prop_battery_level)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_speed(self) -> Optional[str]:
|
||||||
|
"""Return the current fan speed of the vacuum cleaner."""
|
||||||
|
return self.get_map_description(
|
||||||
|
map_=self._fan_level_map,
|
||||||
|
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Water heater entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
WaterHeaterEntity,
|
||||||
|
WaterHeaterEntityFeature
|
||||||
|
)
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('water_heater', []):
|
||||||
|
new_entities.append(WaterHeater(
|
||||||
|
miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||||
|
"""Water heater entities for Xiaomi Home."""
|
||||||
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
|
_prop_temp: Optional[MIoTSpecProperty]
|
||||||
|
_prop_target_temp: Optional[MIoTSpecProperty]
|
||||||
|
_prop_mode: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
_mode_list: Optional[dict[any, any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Water heater."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._attr_temperature_unit = None
|
||||||
|
self._attr_supported_features = WaterHeaterEntityFeature(0)
|
||||||
|
self._prop_on = None
|
||||||
|
self._prop_temp = None
|
||||||
|
self._prop_target_temp = None
|
||||||
|
self._prop_mode = None
|
||||||
|
self._mode_list = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
# on
|
||||||
|
if prop.name == 'on':
|
||||||
|
self._prop_on = prop
|
||||||
|
# temperature
|
||||||
|
if prop.name == 'temperature':
|
||||||
|
if isinstance(prop.value_range, dict):
|
||||||
|
self._attr_min_temp = prop.value_range['min']
|
||||||
|
self._attr_max_temp = prop.value_range['max']
|
||||||
|
if (
|
||||||
|
self._attr_temperature_unit is None
|
||||||
|
and prop.external_unit
|
||||||
|
):
|
||||||
|
self._attr_temperature_unit = prop.external_unit
|
||||||
|
self._prop_temp = prop
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid temperature value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
# target-temperature
|
||||||
|
if prop.name == 'target-temperature':
|
||||||
|
if self._attr_temperature_unit is None and prop.external_unit:
|
||||||
|
self._attr_temperature_unit = prop.external_unit
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE)
|
||||||
|
self._prop_target_temp = prop
|
||||||
|
# mode
|
||||||
|
if prop.name == 'mode':
|
||||||
|
if (
|
||||||
|
not isinstance(prop.value_list, list)
|
||||||
|
or not prop.value_list
|
||||||
|
):
|
||||||
|
_LOGGER.error(
|
||||||
|
'mode value_list is None, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._mode_list = {
|
||||||
|
item['value']: item['description']
|
||||||
|
for item in prop.value_list}
|
||||||
|
self._attr_operation_list = list(self._mode_list.values())
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
WaterHeaterEntityFeature.OPERATION_MODE)
|
||||||
|
self._prop_mode = prop
|
||||||
|
|
||||||
|
async def async_turn_on(self) -> None:
|
||||||
|
"""Turn the water heater on."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self) -> None:
|
||||||
|
"""Turn the water heater off."""
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: any) -> None:
|
||||||
|
"""Set the temperature the water heater should heat water to."""
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
|
||||||
|
|
||||||
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
|
"""Set the operation mode of the water heater.
|
||||||
|
Must be in the operation_list.
|
||||||
|
"""
|
||||||
|
await self.set_property_async(
|
||||||
|
prop=self._prop_mode,
|
||||||
|
value=self.__get_mode_value(description=operation_mode))
|
||||||
|
|
||||||
|
async def async_turn_away_mode_on(self) -> None:
|
||||||
|
"""Set the water heater to away mode."""
|
||||||
|
await self.hass.async_add_executor_job(self.turn_away_mode_on)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.get_prop_value(prop=self._prop_temp)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the target temperature."""
|
||||||
|
return self.get_prop_value(prop=self._prop_target_temp)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self) -> Optional[str]:
|
||||||
|
"""Return the current mode."""
|
||||||
|
return self.__get_mode_description(
|
||||||
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
|
|
||||||
|
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||||
|
"""Convert mode value to description."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
return self._mode_list.get(key, None)
|
||||||
|
|
||||||
|
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||||
|
"""Convert mode description to value."""
|
||||||
|
if self._mode_list is None:
|
||||||
|
return None
|
||||||
|
for key, value in self._mode_list.items():
|
||||||
|
if value == description:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
### Added
|
||||||
|
- first version
|
||||||
|
### Changed
|
||||||
|
### Fixed
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
# Contribution Guidelines
|
||||||
|
|
||||||
|
[English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)
|
||||||
|
|
||||||
|
Thank you for considering contributing to our project! We appreciate your efforts to make our project better.
|
||||||
|
|
||||||
|
Before you start contributing, please take a moment to review the following guidelines.
|
||||||
|
|
||||||
|
## How Can I Contribute?
|
||||||
|
|
||||||
|
### Reporting Bugs
|
||||||
|
|
||||||
|
If you encounter a bug in the project, please [open an issue](https://github.com/XiaoMi/ha_xiaomi_home/issues/new/) on GitHub and provide the detailed information about the bug, including the steps to reproduce the bug, the logs of debug level and the time when it occurs.
|
||||||
|
|
||||||
|
The [method](https://www.home-assistant.io/integrations/logger/#log-filters) to set the integration's log level:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Set the log level in configuration.yaml
|
||||||
|
|
||||||
|
logger:
|
||||||
|
default: critical
|
||||||
|
logs:
|
||||||
|
custom_components.xiaomi_home: debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suggesting Enhancements
|
||||||
|
|
||||||
|
If you have ideas for enhancements or new features, you are welcomed to [start a discussion on ideas](https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas) on GitHub to discuss your ideas.
|
||||||
|
|
||||||
|
### Contributing Code
|
||||||
|
|
||||||
|
1. Fork the repository and create your branch from `main`.
|
||||||
|
2. Ensure that your code adheres to the project coding standard.
|
||||||
|
3. Make sure that your commit messages are descriptive and meaningful.
|
||||||
|
4. Pull requests should be accompanied by a clear description of the problem and the solution.
|
||||||
|
5. Update the documents if necessary.
|
||||||
|
6. Run tests if they are available and ensure they pass.
|
||||||
|
|
||||||
|
## Pull Request Guidelines
|
||||||
|
|
||||||
|
Before submitting a pull request, please make sure that the following requirements are met:
|
||||||
|
|
||||||
|
- Your pull request addresses a single issue or feature.
|
||||||
|
- You have tested your changes locally.
|
||||||
|
- Your code follows the project's [code style](#code-style). Run [`pylint`](https://github.com/google/pyink) over your code using this [pylintrc](../.pylintrc).
|
||||||
|
- All existing tests pass, and you have added new tests if applicable.
|
||||||
|
- Any dependent changes are documented.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
We follow [Google Style](https://google.github.io/styleguide/pyguide.html) for code style and formatting. Please make sure to adhere to this guideline in your contributions.
|
||||||
|
|
||||||
|
## Commit Message Format
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>: <subject>
|
||||||
|
<BLANK LINE>
|
||||||
|
<body>
|
||||||
|
<BLANK LINE>
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
type: commit type is one of the following
|
||||||
|
|
||||||
|
- feat: A new feature.
|
||||||
|
- fix: A bug fix.
|
||||||
|
- docs: Documentation only changes.
|
||||||
|
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.).
|
||||||
|
- refactor: A code change that neither fixes a bug nor adds a feature.
|
||||||
|
- perf: A code change that improves performance.
|
||||||
|
- test: Adding missing tests or correcting existing tests.
|
||||||
|
- chore: Changes to the build process or auxiliary tools and libraries.
|
||||||
|
- revert: Reverting a previous commit.
|
||||||
|
|
||||||
|
subject: A short summary in imperative, present tense. Not capitalized. No period at the end.
|
||||||
|
|
||||||
|
body: A detailed description of the commit and the motivation for the change. The body is mandatory for all commits except for those of type "docs".
|
||||||
|
|
||||||
|
footer: Optional. The footer is the place to reference GitHub issues and PRs that this commit closes or is related to.
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Xiaomi Naming Convention
|
||||||
|
|
||||||
|
- When describing Xiaomi, always use "Xiaomi" in full. Variable names can use "xiaomi" or "mi".
|
||||||
|
- When describing Xiaomi Home, always use "Xiaomi Home". Variable names can use "mihome" or "MiHome".
|
||||||
|
- When describing Xiaomi IoT, always use "MIoT". Variable names can use "miot" or "MIoT".
|
||||||
|
|
||||||
|
### Third-Party Platform Naming Convention
|
||||||
|
|
||||||
|
- When describing Home Assistant, always use "Home Assistant". Variables can use "hass" or "hass_xxx".
|
||||||
|
|
||||||
|
### Other Naming Conventions
|
||||||
|
|
||||||
|
- When using mixed Chinese and English sentences in the document, there must be a space between Chinese and English or the English words must be quoted by Chinese quotation marks. (It is best to write code comments this way too.)
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
|
||||||
|
|
||||||
|
## How to Get Help
|
||||||
|
|
||||||
|
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.
|
||||||
|
|
||||||
|
You can also contact ha_xiaomi_home@xiaomi.com
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "Xiaomi Home",
|
||||||
|
"homeassistant": "2024.8.1",
|
||||||
|
"hacs": "1.34.0"
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check the number of input parameters.
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "usage: $0 [config_path]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Get the config path.
|
||||||
|
config_path=$1
|
||||||
|
# Check if config path exists.
|
||||||
|
if [ ! -d "$config_path" ]; then
|
||||||
|
echo "$config_path does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove the old version.
|
||||||
|
rm -rf "$config_path/custom_components/xiaomi_home"
|
||||||
|
# Get the script path.
|
||||||
|
script_path=$(dirname "$0")
|
||||||
|
# Change to the script path.
|
||||||
|
cd "$script_path"
|
||||||
|
# Copy the new version.
|
||||||
|
cp -r custom_components/xiaomi_home/ "$config_path/custom_components/"
|
||||||
|
|
||||||
|
# Done.
|
||||||
|
echo "Xiaomi Home installation is completed. Please restart Home Assistant."
|
||||||
|
exit 0
|
||||||
Loading…
Reference in New Issue