mirror of
https://github.com/home-assistant/core.git
synced 2026-01-10 09:37:16 +01:00
Compare commits
282 Commits
backup_ign
...
echo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e5673cbd | ||
|
|
800fe1b01e | ||
|
|
15ca2fe489 | ||
|
|
bd919159e5 | ||
|
|
6ebda9322d | ||
|
|
4ca39636e2 | ||
|
|
f7a6d163bb | ||
|
|
746d1800f9 | ||
|
|
91668e99e3 | ||
|
|
0797c3228b | ||
|
|
8ce2727447 | ||
|
|
5b0eca7f85 | ||
|
|
b1b65e4d56 | ||
|
|
17c1c0e155 | ||
|
|
5a0a3d27d9 | ||
|
|
d821aa9162 | ||
|
|
93b01a3bc3 | ||
|
|
98c6a578b7 | ||
|
|
92788a04ff | ||
|
|
a0c2781355 | ||
|
|
6c0c4bfd74 | ||
|
|
f3dd772b43 | ||
|
|
648c750a0f | ||
|
|
f369ded93d | ||
|
|
4b342b7dd4 | ||
|
|
f7e8bc458f | ||
|
|
ee206a5a17 | ||
|
|
883e14b409 | ||
|
|
f5bdd4594d | ||
|
|
c806638448 | ||
|
|
539adaf128 | ||
|
|
7e5617fd54 | ||
|
|
4a0b1b74e3 | ||
|
|
f5263203f5 | ||
|
|
9a1f2b52cd | ||
|
|
037bdb6996 | ||
|
|
3160b7baa0 | ||
|
|
baa3b15dbc | ||
|
|
bf83f5a671 | ||
|
|
463d9617ac | ||
|
|
6e71893b50 | ||
|
|
8078e41cad | ||
|
|
2bd9918ee8 | ||
|
|
98ab16cf99 | ||
|
|
58274160a0 | ||
|
|
fb5af9acd0 | ||
|
|
672df7355c | ||
|
|
7495ea2cc8 | ||
|
|
42ab3228a0 | ||
|
|
a92c52e65b | ||
|
|
800f680bd5 | ||
|
|
26c60880e4 | ||
|
|
059a6dddbe | ||
|
|
0f7cb6b757 | ||
|
|
8068f82888 | ||
|
|
d522571308 | ||
|
|
debee25086 | ||
|
|
508b6c8db0 | ||
|
|
97a124b28a | ||
|
|
800749728b | ||
|
|
b73c6ed768 | ||
|
|
1d43cb3f29 | ||
|
|
56e36cb1ff | ||
|
|
4f43c971cd | ||
|
|
113e703d5c | ||
|
|
e59ec8f867 | ||
|
|
b35d252549 | ||
|
|
71bdd0e237 | ||
|
|
9105542bab | ||
|
|
9cbed483fb | ||
|
|
c687f37539 | ||
|
|
97b853e2ea | ||
|
|
9d241a77b7 | ||
|
|
1cae504cfe | ||
|
|
509add8e5c | ||
|
|
97bf557b32 | ||
|
|
aec7fc1835 | ||
|
|
ab299d2bf7 | ||
|
|
490e012e54 | ||
|
|
e8ff31b792 | ||
|
|
5f98d5a65a | ||
|
|
5d1eb69281 | ||
|
|
ec7ec993b0 | ||
|
|
ff4f4111d0 | ||
|
|
66f293c8f3 | ||
|
|
8826714704 | ||
|
|
f828b4e0b9 | ||
|
|
73442e8443 | ||
|
|
0d8c449ff4 | ||
|
|
fb57284561 | ||
|
|
b856de225d | ||
|
|
9f7c4648a2 | ||
|
|
2d0967994e | ||
|
|
d2bd45099b | ||
|
|
6d6dfce7d1 | ||
|
|
d9a18c2994 | ||
|
|
affec21a6a | ||
|
|
94869f3210 | ||
|
|
e53617a788 | ||
|
|
e916b57714 | ||
|
|
119b296c26 | ||
|
|
20f273f06a | ||
|
|
6aae319b1a | ||
|
|
b3e245687c | ||
|
|
1a56dcfdaf | ||
|
|
66af5ca1e9 | ||
|
|
d24a14442f | ||
|
|
c7169a4ed7 | ||
|
|
08358514b4 | ||
|
|
1392bab4d5 | ||
|
|
e79a1a52c3 | ||
|
|
872cca9935 | ||
|
|
1bf7e5d749 | ||
|
|
2f7a8b4d9d | ||
|
|
0949f7d0ba | ||
|
|
a2ceeb19dc | ||
|
|
1c3d6b5641 | ||
|
|
14375e76a3 | ||
|
|
e5c0183e0f | ||
|
|
5c8fa717bf | ||
|
|
5d851b6a56 | ||
|
|
5dfd358fc9 | ||
|
|
901011de7b | ||
|
|
ad7780291e | ||
|
|
eb6993f0a8 | ||
|
|
406f894dc1 | ||
|
|
0a0a96fb3b | ||
|
|
354855ff5f | ||
|
|
8e6f2e6ff2 | ||
|
|
0b6f49fec2 | ||
|
|
b2e2ef3119 | ||
|
|
e360348525 | ||
|
|
4ed4c2cc5c | ||
|
|
bc5146db3c | ||
|
|
f98e83514d | ||
|
|
e847a8d6a5 | ||
|
|
7117708937 | ||
|
|
d2ce89882b | ||
|
|
1d3fcc67b8 | ||
|
|
32b854515b | ||
|
|
6c3a9cb1a8 | ||
|
|
81c909e8ce | ||
|
|
85f44fa008 | ||
|
|
fb3b23aef3 | ||
|
|
b70c5710a9 | ||
|
|
600bfed704 | ||
|
|
af0a862aab | ||
|
|
1733f5d3fb | ||
|
|
97c558b694 | ||
|
|
d655c51ef9 | ||
|
|
618bdba4d3 | ||
|
|
38efe94def | ||
|
|
0c28b69269 | ||
|
|
36c7546e26 | ||
|
|
8d39f298c0 | ||
|
|
68085ed4f9 | ||
|
|
d97194303a | ||
|
|
b6cb2bfe5b | ||
|
|
c5222708ed | ||
|
|
6cf31e0807 | ||
|
|
ff83a14570 | ||
|
|
46599a4ac4 | ||
|
|
689421eddf | ||
|
|
ee5e25aca6 | ||
|
|
a6bb5dbe2a | ||
|
|
f8ffbf0506 | ||
|
|
6613b46071 | ||
|
|
1579e90d58 | ||
|
|
b71d5737a5 | ||
|
|
8e887f550e | ||
|
|
1af8b69dd6 | ||
|
|
6ef401251c | ||
|
|
141bcae793 | ||
|
|
8ae52cdc4c | ||
|
|
13fe2a9929 | ||
|
|
df50863872 | ||
|
|
82ac3e3fdf | ||
|
|
c48797804d | ||
|
|
e6217efcd6 | ||
|
|
8dd1e9d101 | ||
|
|
096468baa4 | ||
|
|
3659fa4c4e | ||
|
|
d1f0e0a70f | ||
|
|
a45fb57595 | ||
|
|
e9fcef1b57 | ||
|
|
a003f89a5e | ||
|
|
22c634e626 | ||
|
|
46c604fcbe | ||
|
|
94d3b3919d | ||
|
|
350b935fa7 | ||
|
|
e660096801 | ||
|
|
f5e1fa6a21 | ||
|
|
800cdee409 | ||
|
|
33df208296 | ||
|
|
0dc1151a25 | ||
|
|
25865b4849 | ||
|
|
5658f9ca40 | ||
|
|
f9047d0223 | ||
|
|
bbfb9fbdae | ||
|
|
772e7147bd | ||
|
|
9ac60f1c7f | ||
|
|
3b6e3fe457 | ||
|
|
da9fbf21df | ||
|
|
d7e796e9f9 | ||
|
|
e0795e6d07 | ||
|
|
ff16e587e8 | ||
|
|
04b826daa1 | ||
|
|
25296e1b8f | ||
|
|
67fcbc4c28 | ||
|
|
34a33e0465 | ||
|
|
82f2e72327 | ||
|
|
9422c4de65 | ||
|
|
4cdc3de94a | ||
|
|
51aea58c7a | ||
|
|
7e388f69b0 | ||
|
|
d8d054e7dd | ||
|
|
4a385ed26c | ||
|
|
df6cb0b824 | ||
|
|
a7f63e3847 | ||
|
|
b4fac38d8a | ||
|
|
168e45b0f9 | ||
|
|
1fe644d056 | ||
|
|
cd13eff8ae | ||
|
|
e77193fa2e | ||
|
|
66d16336ea | ||
|
|
ed3ca76696 | ||
|
|
f2126a357a | ||
|
|
89956adf2e | ||
|
|
c357b3ae65 | ||
|
|
6b90e7b2c2 | ||
|
|
56b51227bb | ||
|
|
93f1597e6d | ||
|
|
bdeb24cb61 | ||
|
|
09df6c8706 | ||
|
|
0b7ec96448 | ||
|
|
ccd0e27e84 | ||
|
|
e0b50ee1e2 | ||
|
|
7063636db6 | ||
|
|
f67fb9985e | ||
|
|
2d5e920de0 | ||
|
|
9e15a33c42 | ||
|
|
e767863ea4 | ||
|
|
7f3270e982 | ||
|
|
95b1cf465b | ||
|
|
3ce8e1683a | ||
|
|
21032ea7cd | ||
|
|
c75707ec79 | ||
|
|
9573f7828b | ||
|
|
d435f7be09 | ||
|
|
a3eb73cfcc | ||
|
|
fdaa640c8e | ||
|
|
6059446ae3 | ||
|
|
827865a1b9 | ||
|
|
0a78f2725d | ||
|
|
78337a6846 | ||
|
|
78c4d815ce | ||
|
|
482df7408a | ||
|
|
05696b5528 | ||
|
|
c89d8edb3c | ||
|
|
08f6e9cd12 | ||
|
|
cbb0dee911 | ||
|
|
798d2326ed | ||
|
|
91ba9b2239 | ||
|
|
7a23348b1d | ||
|
|
30a6a6ad4b | ||
|
|
4a4c2ff552 | ||
|
|
e16343ed72 | ||
|
|
28dd44504e | ||
|
|
b916fbe1fc | ||
|
|
58797a14e7 | ||
|
|
c090fbfbad | ||
|
|
2bfe96dded | ||
|
|
d99044572a | ||
|
|
11aa08cf74 | ||
|
|
5dc1689e7c | ||
|
|
28ea55aac0 | ||
|
|
7bd2c1d710 | ||
|
|
23d43b23ee | ||
|
|
7dd678ccdf | ||
|
|
371490a470 | ||
|
|
48f58c7d49 | ||
|
|
fae68c8ad5 |
BIN
.github/assets/screenshot-integrations.png
vendored
BIN
.github/assets/screenshot-integrations.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 99 KiB |
100
.github/copilot-instructions.md
vendored
Normal file
100
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
# Instructions for GitHub Copilot
|
||||
|
||||
This repository holds the core of Home Assistant, a Python 3 based home
|
||||
automation application.
|
||||
|
||||
- Python code must be compatible with Python 3.13
|
||||
- Use the newest Python language features if possible:
|
||||
- Pattern matching
|
||||
- Type hints
|
||||
- f-strings for string formatting over `%` or `.format()`
|
||||
- Dataclasses
|
||||
- Walrus operator
|
||||
- Code quality tools:
|
||||
- Formatting: Ruff
|
||||
- Linting: PyLint and Ruff
|
||||
- Type checking: MyPy
|
||||
- Testing: pytest with plain functions and fixtures
|
||||
- Inline code documentation:
|
||||
- File headers should be short and concise:
|
||||
```python
|
||||
"""Integration for Peblar EV chargers."""
|
||||
```
|
||||
- Every method and function needs a docstring:
|
||||
```python
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
|
||||
"""Set up Peblar from a config entry."""
|
||||
...
|
||||
```
|
||||
- All code and comments and other text are written in American English
|
||||
- Follow existing code style patterns as much as possible
|
||||
- Core locations:
|
||||
- Shared constants: `homeassistant/const.py`, use them instead of hardcoding
|
||||
strings or creating duplicate integration constants.
|
||||
- Integration files:
|
||||
- Constants: `homeassistant/components/{domain}/const.py`
|
||||
- Models: `homeassistant/components/{domain}/models.py`
|
||||
- Coordinator: `homeassistant/components/{domain}/coordinator.py`
|
||||
- Config flow: `homeassistant/components/{domain}/config_flow.py`
|
||||
- Platform code: `homeassistant/components/{domain}/{platform}.py`
|
||||
- All external I/O operations must be async
|
||||
- Async patterns:
|
||||
- Avoid sleeping in loops
|
||||
- Avoid awaiting in loops, gather instead
|
||||
- No blocking calls
|
||||
- Polling:
|
||||
- Follow update coordinator pattern, when possible
|
||||
- Polling interval may not be configurable by the user
|
||||
- For local network polling, the minimum interval is 5 seconds
|
||||
- For cloud polling, the minimum interval is 60 seconds
|
||||
- Error handling:
|
||||
- Use specific exceptions from `homeassistant.exceptions`
|
||||
- Setup failures:
|
||||
- Temporary: Raise `ConfigEntryNotReady`
|
||||
- Permanent: Use `ConfigEntryError`
|
||||
- Logging:
|
||||
- Message format:
|
||||
- No periods at end
|
||||
- No integration names or domains (added automatically)
|
||||
- No sensitive data (keys, tokens, passwords), even when those are incorrect.
|
||||
- Be very restrictive on the use of logging info messages, use debug for
|
||||
anything which is not targeting the user.
|
||||
- Use lazy logging (no f-strings):
|
||||
```python
|
||||
_LOGGER.debug("This is a log message with %s", variable)
|
||||
```
|
||||
- Entities:
|
||||
- Ensure unique IDs for state persistence:
|
||||
- Unique IDs should not contain values that are subject to user or network change.
|
||||
- An ID needs to be unique per platform, not per integration.
|
||||
- The ID does not have to contain the integration domain or platform.
|
||||
- Acceptable examples:
|
||||
- Serial number of a device
|
||||
- MAC address of a device formatted using `homeassistant.helpers.device_registry.format_mac`
|
||||
Do not obtain the MAC address through arp cache of local network access,
|
||||
only use the MAC address provided by discovery or the device itself.
|
||||
- Unique identifier that is physically printed on the device or burned into an EEPROM
|
||||
- Not acceptable examples:
|
||||
- IP Address
|
||||
- Device name
|
||||
- Hostname
|
||||
- URL
|
||||
- Email address
|
||||
- Username
|
||||
- For entities that are setup by a config entry, the config entry ID
|
||||
can be used as a last resort if no other Unique ID is available.
|
||||
For example: `f"{entry.entry_id}-battery"`
|
||||
- If the state value is unknown, use `None`
|
||||
- Do not use the `unavailable` string as a state value,
|
||||
implement the `available()` property method instead
|
||||
- Do not use the `unknown` string as a state value, use `None` instead
|
||||
- Extra entity state attributes:
|
||||
- The keys of all state attributes should always be present
|
||||
- If the value is unknown, use `None`
|
||||
- Provide descriptive state attributes
|
||||
- Testing:
|
||||
- Test location: `tests/components/{domain}/`
|
||||
- Use pytest fixtures from `tests.common`
|
||||
- Mock external dependencies
|
||||
- Use snapshots for complex data
|
||||
- Follow existing test patterns
|
||||
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
44
.github/workflows/ci.yaml
vendored
44
.github/workflows/ci.yaml
vendored
@@ -240,7 +240,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -482,7 +482,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -490,7 +490,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -578,7 +578,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -611,7 +611,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -649,7 +649,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -692,7 +692,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -739,7 +739,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -791,7 +791,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -799,7 +799,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -865,7 +865,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -929,7 +929,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1051,7 +1051,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1181,7 +1181,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1328,7 +1328,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.1
|
||||
rev: v0.9.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -234,6 +234,7 @@ homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.history_stats.*
|
||||
homeassistant.components.holiday.*
|
||||
homeassistant.components.home_connect.*
|
||||
homeassistant.components.homeassistant.*
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
homeassistant.components.homeassistant_green.*
|
||||
@@ -437,6 +438,7 @@ homeassistant.components.select.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.sensorpush_cloud.*
|
||||
homeassistant.components.sensoterra.*
|
||||
homeassistant.components.senz.*
|
||||
homeassistant.components.sfr_box.*
|
||||
|
||||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -42,6 +42,14 @@
|
||||
"--picked"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant: Debug Current Test File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["-vv", "${file}"]
|
||||
},
|
||||
{
|
||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
@@ -77,4 +85,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1342,6 +1342,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sensorpro/ @bdraco
|
||||
/homeassistant/components/sensorpush/ @bdraco
|
||||
/tests/components/sensorpush/ @bdraco
|
||||
/homeassistant/components/sensorpush_cloud/ @sstallion
|
||||
/tests/components/sensorpush_cloud/ @sstallion
|
||||
/homeassistant/components/sensoterra/ @markruys
|
||||
/tests/components/sensoterra/ @markruys
|
||||
/homeassistant/components/sentry/ @dcramer @frenck
|
||||
|
||||
38
Dockerfile
generated
38
Dockerfile
generated
@@ -12,8 +12,26 @@ ENV \
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.8/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.5.27
|
||||
RUN pip3 install uv==0.6.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
@@ -42,22 +60,4 @@ RUN \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.8/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
WORKDIR /config
|
||||
|
||||
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
5
homeassistant/brands/sensorpush.json
Normal file
5
homeassistant/brands/sensorpush.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "sensorpush",
|
||||
"name": "SensorPush",
|
||||
"integrations": ["sensorpush", "sensorpush_cloud"]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@@ -123,12 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||
"""Unload AdGuard Home config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
loaded_entries = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
# This is the last loaded instance of AdGuard, deregister any services
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.9.1"],
|
||||
"requirements": ["airgradient==0.9.2"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||
return self.async_create_entry(title=device_info["name"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioairq import AirQ
|
||||
from aioairq.core import AirQ, identify_warming_up_sensors
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
@@ -55,6 +55,9 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch the data from the device."""
|
||||
if "name" not in self.device_info:
|
||||
_LOGGER.debug(
|
||||
"'name' not found in AirQCoordinator.device_info, fetching from the device"
|
||||
)
|
||||
info = await self.airq.fetch_device_info()
|
||||
self.device_info.update(
|
||||
DeviceInfo(
|
||||
@@ -64,7 +67,16 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
hw_version=info["hw_version"],
|
||||
)
|
||||
)
|
||||
return await self.airq.get_latest_data( # type: ignore[no-any-return]
|
||||
_LOGGER.debug(
|
||||
"Updated AirQCoordinator.device_info for 'name' %s",
|
||||
self.device_info.get("name"),
|
||||
)
|
||||
data: dict = await self.airq.get_latest_data(
|
||||
return_average=self.return_average,
|
||||
clip_negative_values=self.clip_negative,
|
||||
)
|
||||
if warming_up_sensors := identify_warming_up_sensors(data):
|
||||
_LOGGER.debug(
|
||||
"Following sensors are still warming up: %s", warming_up_sensors
|
||||
)
|
||||
return data
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -28,11 +28,13 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"alarm_arm_home": {
|
||||
"name": "Arm home",
|
||||
"description": "Sets the alarm to: _armed, but someone is home_.",
|
||||
"description": "Arms the alarm in the home mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
"alarm_arm_away": {
|
||||
"name": "Arm away",
|
||||
"description": "Sets the alarm to: _armed, no one home_.",
|
||||
"description": "Arms the alarm in the away mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"alarm_arm_night": {
|
||||
"name": "Arm night",
|
||||
"description": "Sets the alarm to: _armed for the night_.",
|
||||
"description": "Arms the alarm in the night mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"alarm_arm_vacation": {
|
||||
"name": "Arm vacation",
|
||||
"description": "Sets the alarm to: _armed for vacation_.",
|
||||
"description": "Arms the alarm in the vacation mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -130,7 +130,7 @@
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"name": "Trigger",
|
||||
"description": "Trigger the alarm manually.",
|
||||
"description": "Triggers the alarm manually.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
|
||||
@@ -239,6 +239,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
native_unit_of_measurement=DEGREE,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
|
||||
@@ -608,21 +608,25 @@ SENSOR_DESCRIPTIONS = (
|
||||
key=TYPE_WINDDIR,
|
||||
translation_key="wind_direction",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG10M,
|
||||
translation_key="wind_direction_average_10m",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG2M,
|
||||
translation_key="wind_direction_average_2m",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTDIR,
|
||||
translation_key="wind_gust_direction",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on"
|
||||
"cannot_receive_deviceinfo": "Failed to retrieve MAC Address. Make sure the device is turned on"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aranet4.client import Aranet4Advertisement
|
||||
from aranet4.client import Aranet4Advertisement, Color
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
@@ -74,6 +74,13 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"status": AranetSensorEntityDescription(
|
||||
key="threshold",
|
||||
translation_key="threshold",
|
||||
name="Threshold",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[status.name.lower() for status in Color],
|
||||
),
|
||||
"co2": AranetSensorEntityDescription(
|
||||
key="co2",
|
||||
name="Carbon Dioxide",
|
||||
@@ -161,7 +168,10 @@ def sensor_update_to_bluetooth_data_update(
|
||||
val = getattr(adv.readings, key)
|
||||
if val == -1:
|
||||
continue
|
||||
val *= desc.scale
|
||||
if key == "status":
|
||||
val = val.name.lower()
|
||||
else:
|
||||
val *= desc.scale
|
||||
data[tag] = val
|
||||
names[tag] = desc.name
|
||||
descs[tag] = desc
|
||||
|
||||
@@ -21,5 +21,17 @@
|
||||
"no_devices_found": "No unconfigured Aranet devices found.",
|
||||
"outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"threshold": {
|
||||
"state": {
|
||||
"error": "Error",
|
||||
"green": "Green",
|
||||
"yellow": "Yellow",
|
||||
"red": "Red"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.5.2"],
|
||||
"requirements": ["arcam-fmj==1.8.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -92,7 +92,12 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
),
|
||||
ArwnSensor(
|
||||
topic + "/dir", "Wind Direction", "direction", DEGREE, "mdi:compass"
|
||||
topic + "/dir",
|
||||
"Wind Direction",
|
||||
"direction",
|
||||
DEGREE,
|
||||
"mdi:compass",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
]
|
||||
return None
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import Any, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
import wave
|
||||
|
||||
import hass_nabucasa
|
||||
@@ -30,7 +30,7 @@ from homeassistant.components import (
|
||||
from homeassistant.components.tts import (
|
||||
generate_media_source_id as tts_generate_media_source_id,
|
||||
)
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, intent
|
||||
@@ -81,6 +81,9 @@ from .error import (
|
||||
)
|
||||
from .vad import AudioBuffer, VoiceActivityTimeout, VoiceCommandSegmenter, chunk_samples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hassil.recognize import RecognizeResult
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_KEY = f"{DOMAIN}.pipelines"
|
||||
@@ -123,6 +126,12 @@ STORED_PIPELINE_RUNS = 10
|
||||
SAVE_DELAY = 10
|
||||
|
||||
|
||||
@callback
|
||||
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
||||
"""Filter out intents that are not local fallback."""
|
||||
return result.intent.name in (intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_resolve_default_pipeline_settings(
|
||||
hass: HomeAssistant,
|
||||
@@ -1084,10 +1093,22 @@ class PipelineRun:
|
||||
)
|
||||
intent_response.async_set_speech(trigger_response_text)
|
||||
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None
|
||||
# If the LLM has API access, we filter out some sentences that are
|
||||
# interfering with LLM operation.
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
intent_filter = _async_local_fallback_intent_filter
|
||||
|
||||
# Try local intents first, if preferred.
|
||||
elif self.pipeline.prefer_local_intents and (
|
||||
intent_response := await conversation.async_handle_intents(
|
||||
self.hass, user_input
|
||||
self.hass,
|
||||
user_input,
|
||||
intent_filter=intent_filter,
|
||||
)
|
||||
):
|
||||
# Local intent matched
|
||||
|
||||
@@ -19,6 +19,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import AssistSatelliteConfiguration
|
||||
|
||||
CONNECTION_TEST_TIMEOUT = 30
|
||||
|
||||
@@ -91,7 +92,16 @@ def websocket_get_configuration(
|
||||
)
|
||||
return
|
||||
|
||||
config_dict = asdict(satellite.async_get_configuration())
|
||||
try:
|
||||
config_dict = asdict(satellite.async_get_configuration())
|
||||
except NotImplementedError:
|
||||
# Stub configuration
|
||||
config_dict = asdict(
|
||||
AssistSatelliteConfiguration(
|
||||
available_wake_words=[], active_wake_words=[], max_active_wake_words=1
|
||||
)
|
||||
)
|
||||
|
||||
config_dict["pipeline_entity_id"] = satellite.pipeline_entity_id
|
||||
config_dict["vad_entity_id"] = satellite.vad_sensitivity_entity_id
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)",
|
||||
"invalid_serial_port": "Serial port is not a valid device or could not be openned",
|
||||
"invalid_serial_port": "Serial port is not a valid device or could not be opened",
|
||||
"cannot_open_serial_port": "Cannot open serial port, please check and try again"
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -16,6 +16,7 @@ from .agent import (
|
||||
BackupAgentPlatformProtocol,
|
||||
LocalBackupAgent,
|
||||
)
|
||||
from .config import BackupConfig, CreateBackupParametersDict
|
||||
from .const import DATA_MANAGER, DOMAIN
|
||||
from .http import async_register_http_views
|
||||
from .manager import (
|
||||
@@ -47,12 +48,14 @@ __all__ = [
|
||||
"BackupAgent",
|
||||
"BackupAgentError",
|
||||
"BackupAgentPlatformProtocol",
|
||||
"BackupConfig",
|
||||
"BackupManagerError",
|
||||
"BackupNotFound",
|
||||
"BackupPlatformProtocol",
|
||||
"BackupReaderWriter",
|
||||
"BackupReaderWriterError",
|
||||
"CreateBackupEvent",
|
||||
"CreateBackupParametersDict",
|
||||
"CreateBackupStage",
|
||||
"CreateBackupState",
|
||||
"Folder",
|
||||
|
||||
@@ -154,7 +154,8 @@ class BackupConfig:
|
||||
self.data.retention.apply(self._manager)
|
||||
self.data.schedule.apply(self._manager)
|
||||
|
||||
async def update(
|
||||
@callback
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED,
|
||||
|
||||
@@ -43,7 +43,11 @@ from .agent import (
|
||||
BackupAgentPlatformProtocol,
|
||||
LocalBackupAgent,
|
||||
)
|
||||
from .config import BackupConfig, delete_backups_exceeding_configured_count
|
||||
from .config import (
|
||||
BackupConfig,
|
||||
CreateBackupParametersDict,
|
||||
delete_backups_exceeding_configured_count,
|
||||
)
|
||||
from .const import (
|
||||
BUF_SIZE,
|
||||
DATA_MANAGER,
|
||||
@@ -282,6 +286,10 @@ class BackupReaderWriter(abc.ABC):
|
||||
) -> None:
|
||||
"""Get restore events after core restart."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||
"""Validate backup config."""
|
||||
|
||||
|
||||
class IncorrectPasswordError(BackupReaderWriterError):
|
||||
"""Raised when the password is incorrect."""
|
||||
@@ -333,6 +341,7 @@ class BackupManager:
|
||||
self.config.load(stored["config"])
|
||||
self.known_backups.load(stored["backups"])
|
||||
|
||||
await self._reader_writer.async_validate_config(config=self.config)
|
||||
await self._reader_writer.async_resume_restore_progress_after_restart(
|
||||
on_progress=self.async_on_backup_event
|
||||
)
|
||||
@@ -1461,11 +1470,9 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
backup_id = _generate_backup_id(date_str, backup_name)
|
||||
|
||||
if include_addons or include_all_addons or include_folders:
|
||||
if not extra_metadata.get("with_automatic_settings"):
|
||||
raise BackupReaderWriterError(
|
||||
"Addons and folders are not supported by core backup"
|
||||
)
|
||||
LOGGER.debug("Ignoring addons and folders in automatic backup")
|
||||
raise BackupReaderWriterError(
|
||||
"Addons and folders are not supported by core backup"
|
||||
)
|
||||
if not include_homeassistant:
|
||||
raise BackupReaderWriterError("Home Assistant must be included in backup")
|
||||
|
||||
@@ -1834,6 +1841,44 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
)
|
||||
on_progress(IdleEvent())
|
||||
|
||||
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||
"""Validate backup config.
|
||||
|
||||
Update automatic backup settings to not include addons or folders and remove
|
||||
hassio agents in case a backup created by supervisor was restored.
|
||||
"""
|
||||
create_backup = config.data.create_backup
|
||||
if (
|
||||
not create_backup.include_addons
|
||||
and not create_backup.include_all_addons
|
||||
and not create_backup.include_folders
|
||||
and not any(a_id.startswith("hassio.") for a_id in create_backup.agent_ids)
|
||||
):
|
||||
LOGGER.debug("Backup settings don't need to be adjusted")
|
||||
return
|
||||
|
||||
LOGGER.info(
|
||||
"Adjusting backup settings to not include addons, folders or supervisor locations"
|
||||
)
|
||||
automatic_agents = [
|
||||
agent_id
|
||||
for agent_id in create_backup.agent_ids
|
||||
if not agent_id.startswith("hassio.")
|
||||
]
|
||||
if (
|
||||
self._local_agent_id not in automatic_agents
|
||||
and "hassio.local" in create_backup.agent_ids
|
||||
):
|
||||
automatic_agents = [self._local_agent_id, *automatic_agents]
|
||||
config.update(
|
||||
create_backup=CreateBackupParametersDict(
|
||||
agent_ids=automatic_agents,
|
||||
include_addons=None,
|
||||
include_all_addons=False,
|
||||
include_folders=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _generate_backup_id(date: str, name: str) -> str:
|
||||
"""Generate a backup ID."""
|
||||
|
||||
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||
STORE_DELAY_SAVE = 30
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION_MINOR = 3
|
||||
STORAGE_VERSION_MINOR = 4
|
||||
|
||||
|
||||
class StoredBackupData(TypedDict):
|
||||
@@ -60,6 +60,13 @@ class _BackupStore(Store[StoredBackupData]):
|
||||
else:
|
||||
data["config"]["schedule"]["days"] = [state]
|
||||
data["config"]["schedule"]["recurrence"] = "custom_days"
|
||||
if old_minor_version < 4:
|
||||
# Workaround for a bug in frontend which incorrectly set days to 0
|
||||
# instead of to None for unlimited retention.
|
||||
if data["config"]["retention"]["copies"] == 0:
|
||||
data["config"]["retention"]["copies"] = None
|
||||
if data["config"]["retention"]["days"] == 0:
|
||||
data["config"]["retention"]["days"] = None
|
||||
|
||||
# Note: We allow reading data with major version 2.
|
||||
# Reject if major version is higher than 2.
|
||||
|
||||
@@ -104,12 +104,15 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
bool, homeassistant.get("exclude_database", False)
|
||||
)
|
||||
|
||||
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
||||
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
||||
|
||||
return AgentBackup(
|
||||
addons=addons,
|
||||
backup_id=cast(str, data["slug"]),
|
||||
database_included=database_included,
|
||||
date=cast(str, data["date"]),
|
||||
extra_metadata=cast(dict[str, bool | str], data.get("extra", {})),
|
||||
date=cast(str, date),
|
||||
extra_metadata=extra_metadata,
|
||||
folders=folders,
|
||||
homeassistant_included=homeassistant_included,
|
||||
homeassistant_version=homeassistant_version,
|
||||
|
||||
@@ -346,6 +346,7 @@ async def handle_config_info(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@@ -368,8 +369,10 @@ async def handle_config_info(
|
||||
),
|
||||
vol.Optional("retention"): vol.Schema(
|
||||
{
|
||||
vol.Optional("copies"): vol.Any(int, None),
|
||||
vol.Optional("days"): vol.Any(int, None),
|
||||
# Note: We can't use cv.positive_int because it allows 0 even
|
||||
# though 0 is not positive.
|
||||
vol.Optional("copies"): vol.Any(vol.All(int, vol.Range(min=1)), None),
|
||||
vol.Optional("days"): vol.Any(vol.All(int, vol.Range(min=1)), None),
|
||||
},
|
||||
),
|
||||
vol.Optional("schedule"): vol.Schema(
|
||||
@@ -385,8 +388,7 @@ async def handle_config_info(
|
||||
),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def handle_config_update(
|
||||
def handle_config_update(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -396,7 +398,7 @@ async def handle_config_update(
|
||||
changes = dict(msg)
|
||||
changes.pop("id")
|
||||
changes.pop("type")
|
||||
await manager.config.update(**changes)
|
||||
manager.config.update(**changes)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ PLATFORMS = [
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pybalboa"],
|
||||
"requirements": ["pybalboa==1.1.2"]
|
||||
"requirements": ["pybalboa==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -78,6 +78,19 @@
|
||||
"high": "High"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"filter_cycle_2_enabled": {
|
||||
"name": "Filter cycle 2 enabled"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"filter_cycle_start": {
|
||||
"name": "Filter cycle {index} start"
|
||||
},
|
||||
"filter_cycle_end": {
|
||||
"name": "Filter cycle {index} end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
homeassistant/components/balboa/switch.py
Normal file
48
homeassistant/components/balboa/switch.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Support for Balboa switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pybalboa import SpaClient
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's switches."""
|
||||
spa = entry.runtime_data
|
||||
async_add_entities([BalboaSwitchEntity(spa)])
|
||||
|
||||
|
||||
class BalboaSwitchEntity(BalboaEntity, SwitchEntity):
|
||||
"""Representation of a Balboa switch entity."""
|
||||
|
||||
def __init__(self, spa: SpaClient) -> None:
|
||||
"""Initialize a Balboa switch entity."""
|
||||
super().__init__(spa, "filter_cycle_2_enabled")
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_translation_key = "filter_cycle_2_enabled"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._client.filter_cycle_2_enabled
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._client.configure_filter_cycle(2, enabled=True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self._client.configure_filter_cycle(2, enabled=False)
|
||||
56
homeassistant/components/balboa/time.py
Normal file
56
homeassistant/components/balboa/time.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Support for Balboa times."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
import itertools
|
||||
from typing import Any
|
||||
|
||||
from pybalboa import SpaClient
|
||||
|
||||
from homeassistant.components.time import TimeEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
FILTER_CYCLE = "filter_cycle_"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's times."""
|
||||
spa = entry.runtime_data
|
||||
async_add_entities(
|
||||
BalboaTimeEntity(spa, index, period)
|
||||
for index, period in itertools.product((1, 2), ("start", "end"))
|
||||
)
|
||||
|
||||
|
||||
class BalboaTimeEntity(BalboaEntity, TimeEntity):
|
||||
"""Representation of a Balboa time entity."""
|
||||
|
||||
entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(self, spa: SpaClient, index: int, period: str) -> None:
|
||||
"""Initialize a Balboa time entity."""
|
||||
super().__init__(spa, f"{FILTER_CYCLE}{index}_{period}")
|
||||
self.index = index
|
||||
self.period = period
|
||||
self._attr_translation_key = f"{FILTER_CYCLE}{period}"
|
||||
self._attr_translation_placeholders = {"index": str(index)}
|
||||
|
||||
@property
|
||||
def native_value(self) -> time | None:
|
||||
"""Return the value reported by the time."""
|
||||
return getattr(self._client, f"{FILTER_CYCLE}{self.index}_{self.period}")
|
||||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the time."""
|
||||
args: dict[str, Any] = {self.period: value}
|
||||
await self._client.configure_filter_cycle(self.index, **args)
|
||||
@@ -4,12 +4,13 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import DEVICE_BUTTONS, DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -25,8 +26,9 @@ async def async_get_config_entry_diagnostics(
|
||||
if TYPE_CHECKING:
|
||||
assert config_entry.unique_id
|
||||
|
||||
# Add media_player entity's state
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Add media_player entity's state
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
MEDIA_PLAYER_DOMAIN, DOMAIN, config_entry.unique_id
|
||||
):
|
||||
@@ -37,4 +39,16 @@ async def async_get_config_entry_diagnostics(
|
||||
state_dict.pop("context")
|
||||
data["media_player"] = state_dict
|
||||
|
||||
# Add button Event entity states (if enabled)
|
||||
for device_button in DEVICE_BUTTONS:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
EVENT_DOMAIN, DOMAIN, f"{config_entry.unique_id}_{device_button}"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data[f"{device_button}_event"] = state_dict
|
||||
|
||||
return data
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.22.3",
|
||||
"bleak-retry-connector==3.8.1",
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-auto-recovery==1.4.4",
|
||||
"bluetooth-data-tools==1.23.4",
|
||||
"dbus-fast==2.33.0",
|
||||
"habluetooth==3.21.1"
|
||||
"habluetooth==3.24.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = (
|
||||
mutually_exclusive=Action.SET_BRIGHTNESS,
|
||||
argument=None,
|
||||
),
|
||||
BondButtonEntityDescription(
|
||||
key=Action.TOGGLE_LIGHT_TEMP,
|
||||
name="Toggle Light Temperature",
|
||||
translation_key="toggle_light_temp",
|
||||
mutually_exclusive=None, # No mutually exclusive action
|
||||
argument=None,
|
||||
),
|
||||
BondButtonEntityDescription(
|
||||
key=Action.START_UP_LIGHT_DIMMER,
|
||||
name="Start Up Light Dimmer",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"services": {
|
||||
"set_fan_speed_tracked_state": {
|
||||
"name": "Set fan speed tracked state",
|
||||
"description": "Sets the tracked fan speed for a bond fan.",
|
||||
"description": "Sets the tracked fan speed for a Bond fan.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"set_switch_power_tracked_state": {
|
||||
"name": "Set switch power tracked state",
|
||||
"description": "Sets the tracked power state of a bond switch.",
|
||||
"description": "Sets the tracked power state of a Bond switch.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"set_light_power_tracked_state": {
|
||||
"name": "Set light power tracked state",
|
||||
"description": "Sets the tracked power state of a bond light.",
|
||||
"description": "Sets the tracked power state of a Bond light.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"set_light_brightness_tracked_state": {
|
||||
"name": "Set light brightness tracked state",
|
||||
"description": "Sets the tracked brightness state of a bond light.",
|
||||
"description": "Sets the tracked brightness state of a Bond light.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -87,15 +87,15 @@
|
||||
},
|
||||
"start_increasing_brightness": {
|
||||
"name": "Start increasing brightness",
|
||||
"description": "Start increasing the brightness of the light. (deprecated)."
|
||||
"description": "Starts increasing the brightness of the light (deprecated)."
|
||||
},
|
||||
"start_decreasing_brightness": {
|
||||
"name": "Start decreasing brightness",
|
||||
"description": "Start decreasing the brightness of the light. (deprecated)."
|
||||
"description": "Starts decreasing the brightness of the light (deprecated)."
|
||||
},
|
||||
"stop": {
|
||||
"name": "[%key:common::action::stop%]",
|
||||
"description": "Stop any in-progress action and empty the queue. (deprecated)."
|
||||
"description": "Stops any in-progress action and empty the queue (deprecated)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ class BroadlinkEntity(Entity):
|
||||
self._device = device
|
||||
self._coordinator = device.update_manager.coordinator
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when the entity is added to hass."""
|
||||
self.async_on_remove(self._coordinator.async_add_listener(self._recv_data))
|
||||
if self._coordinator.data:
|
||||
self._update_state(self._coordinator.data)
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the entity."""
|
||||
await self._coordinator.async_request_refresh()
|
||||
|
||||
@@ -49,7 +49,7 @@ class BroadlinkEntity(Entity):
|
||||
"""
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return True if the entity is available."""
|
||||
return self._device.available
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"message": "Can't set preset mode to {preset_mode} when HVAC mode is not set to auto"
|
||||
},
|
||||
"set_data_error": {
|
||||
"message": "An error occurred while sending the data to the BSBLAN device"
|
||||
"message": "An error occurred while sending the data to the BSB-Lan device"
|
||||
},
|
||||
"set_temperature_error": {
|
||||
"message": "An error occurred while setting the temperature"
|
||||
|
||||
@@ -169,6 +169,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="windazimuth",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pressure",
|
||||
@@ -530,30 +531,35 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="windazimuth_1d",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="windazimuth_2d",
|
||||
translation_key="windazimuth_2d",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="windazimuth_3d",
|
||||
translation_key="windazimuth_3d",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="windazimuth_4d",
|
||||
translation_key="windazimuth_4d",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="windazimuth_5d",
|
||||
translation_key="windazimuth_5d",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="condition_1d",
|
||||
|
||||
@@ -104,7 +104,7 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
"""Open connection to the router and get arp entries."""
|
||||
|
||||
try:
|
||||
cisco_ssh: pxssh.pxssh[str] = pxssh.pxssh(encoding="uft-8")
|
||||
cisco_ssh: pxssh.pxssh[str] = pxssh.pxssh(encoding="utf-8")
|
||||
cisco_ssh.login(
|
||||
self.host,
|
||||
self.username,
|
||||
|
||||
@@ -11,7 +11,11 @@ from typing import Any
|
||||
from aiohttp import ClientError
|
||||
from hass_nabucasa import Cloud, CloudError
|
||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||
from hass_nabucasa.cloud_api import async_files_delete_file, async_files_list
|
||||
from hass_nabucasa.cloud_api import (
|
||||
FilesHandlerListEntry,
|
||||
async_files_delete_file,
|
||||
async_files_list,
|
||||
)
|
||||
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
|
||||
|
||||
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
||||
@@ -76,11 +80,6 @@ class CloudBackupAgent(BackupAgent):
|
||||
self._cloud = cloud
|
||||
self._hass = hass
|
||||
|
||||
@callback
|
||||
def _get_backup_filename(self) -> str:
|
||||
"""Return the backup filename."""
|
||||
return f"{self._cloud.client.prefs.instance_id}.tar"
|
||||
|
||||
async def async_download_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
@@ -91,13 +90,13 @@ class CloudBackupAgent(BackupAgent):
|
||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||
:return: An async iterator that yields bytes.
|
||||
"""
|
||||
if not await self.async_get_backup(backup_id):
|
||||
if not (backup := await self._async_get_backup(backup_id)):
|
||||
raise BackupAgentError("Backup not found")
|
||||
|
||||
try:
|
||||
content = await self._cloud.files.download(
|
||||
storage_type=StorageType.BACKUP,
|
||||
filename=self._get_backup_filename(),
|
||||
filename=backup["Key"],
|
||||
)
|
||||
except CloudError as err:
|
||||
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
||||
@@ -124,7 +123,7 @@ class CloudBackupAgent(BackupAgent):
|
||||
base64md5hash = await calculate_b64md5(open_stream, size)
|
||||
except FilesError as err:
|
||||
raise BackupAgentError(err) from err
|
||||
filename = self._get_backup_filename()
|
||||
filename = f"{self._cloud.client.prefs.instance_id}.tar"
|
||||
metadata = backup.as_dict()
|
||||
|
||||
tries = 1
|
||||
@@ -172,29 +171,34 @@ class CloudBackupAgent(BackupAgent):
|
||||
|
||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||
"""
|
||||
if not await self.async_get_backup(backup_id):
|
||||
if not (backup := await self._async_get_backup(backup_id)):
|
||||
return
|
||||
|
||||
try:
|
||||
await async_files_delete_file(
|
||||
self._cloud,
|
||||
storage_type=StorageType.BACKUP,
|
||||
filename=self._get_backup_filename(),
|
||||
filename=backup["Key"],
|
||||
)
|
||||
except (ClientError, CloudError) as err:
|
||||
raise BackupAgentError("Failed to delete backup") from err
|
||||
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||
"""List backups."""
|
||||
backups = await self._async_list_backups()
|
||||
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
||||
|
||||
async def _async_list_backups(self) -> list[FilesHandlerListEntry]:
|
||||
"""List backups."""
|
||||
try:
|
||||
backups = await async_files_list(
|
||||
self._cloud, storage_type=StorageType.BACKUP
|
||||
)
|
||||
_LOGGER.debug("Cloud backups: %s", backups)
|
||||
except (ClientError, CloudError) as err:
|
||||
raise BackupAgentError("Failed to list backups") from err
|
||||
|
||||
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
||||
_LOGGER.debug("Cloud backups: %s", backups)
|
||||
return backups
|
||||
|
||||
async def async_get_backup(
|
||||
self,
|
||||
@@ -202,10 +206,19 @@ class CloudBackupAgent(BackupAgent):
|
||||
**kwargs: Any,
|
||||
) -> AgentBackup | None:
|
||||
"""Return a backup."""
|
||||
backups = await self.async_list_backups()
|
||||
if not (backup := await self._async_get_backup(backup_id)):
|
||||
return None
|
||||
return AgentBackup.from_dict(backup["Metadata"])
|
||||
|
||||
async def _async_get_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
) -> FilesHandlerListEntry | None:
|
||||
"""Return a backup."""
|
||||
backups = await self._async_list_backups()
|
||||
|
||||
for backup in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
if backup["Metadata"]["backup_id"] == backup_id:
|
||||
return backup
|
||||
|
||||
return None
|
||||
|
||||
@@ -8,14 +8,15 @@ from contextlib import suppress
|
||||
import dataclasses
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Concatenate
|
||||
from typing import Any, Concatenate, cast
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import attr
|
||||
from hass_nabucasa import Cloud, auth, thingtalk
|
||||
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
from hass_nabucasa.voice import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
@@ -64,7 +65,9 @@ from .subscription import async_subscription_info
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = {
|
||||
_CLOUD_ERRORS: dict[
|
||||
type[Exception], tuple[HTTPStatus, Callable[[Exception], str] | str]
|
||||
] = {
|
||||
TimeoutError: (
|
||||
HTTPStatus.BAD_GATEWAY,
|
||||
"Unable to reach the Home Assistant cloud.",
|
||||
@@ -133,6 +136,10 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Multi-factor authentication expired, or not started. Please try again.",
|
||||
),
|
||||
AlreadyConnectedError: (
|
||||
HTTPStatus.CONFLICT,
|
||||
lambda x: json.dumps(cast(AlreadyConnectedError, x).details),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -197,7 +204,11 @@ def _process_cloud_exception(exc: Exception, where: str) -> tuple[HTTPStatus, st
|
||||
|
||||
for err, value_info in _CLOUD_ERRORS.items():
|
||||
if isinstance(exc, err):
|
||||
err_info = value_info
|
||||
status, content = value_info
|
||||
err_info = (
|
||||
status,
|
||||
content if isinstance(content, str) else content(exc),
|
||||
)
|
||||
break
|
||||
|
||||
if err_info is None:
|
||||
@@ -240,6 +251,7 @@ class CloudLoginView(HomeAssistantView):
|
||||
vol.All(
|
||||
{
|
||||
vol.Required("email"): str,
|
||||
vol.Optional("check_connection", default=False): bool,
|
||||
vol.Exclusive("password", "login"): str,
|
||||
vol.Exclusive("code", "login"): str,
|
||||
},
|
||||
@@ -258,7 +270,11 @@ class CloudLoginView(HomeAssistantView):
|
||||
code = data.get("code")
|
||||
|
||||
if email and password:
|
||||
await cloud.login(email, password)
|
||||
await cloud.login(
|
||||
email,
|
||||
password,
|
||||
check_connection=data["check_connection"],
|
||||
)
|
||||
|
||||
else:
|
||||
if (
|
||||
@@ -270,7 +286,12 @@ class CloudLoginView(HomeAssistantView):
|
||||
# Voluptuous should ensure that code is not None because password is
|
||||
assert code is not None
|
||||
|
||||
await cloud.login_verify_totp(email, code, self._mfa_tokens)
|
||||
await cloud.login_verify_totp(
|
||||
email,
|
||||
code,
|
||||
self._mfa_tokens,
|
||||
check_connection=data["check_connection"],
|
||||
)
|
||||
self._mfa_tokens = {}
|
||||
self._mfa_tokens_set_time = 0
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
from hassil.recognize import RecognizeResult
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -241,7 +243,10 @@ async def async_handle_sentence_triggers(
|
||||
|
||||
|
||||
async def async_handle_intents(
|
||||
hass: HomeAssistant, user_input: ConversationInput
|
||||
hass: HomeAssistant,
|
||||
user_input: ConversationInput,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
) -> intent.IntentResponse | None:
|
||||
"""Try to match input against registered intents and return response.
|
||||
|
||||
@@ -250,7 +255,9 @@ async def async_handle_intents(
|
||||
default_agent = async_get_agent(hass)
|
||||
assert isinstance(default_agent, DefaultAgent)
|
||||
|
||||
return await default_agent.async_handle_intents(user_input)
|
||||
return await default_agent.async_handle_intents(
|
||||
user_input, intent_filter=intent_filter
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
@@ -53,6 +53,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_added_domain
|
||||
from homeassistant.util import language as language_util
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .chat_log import AssistantContent, async_get_chat_log
|
||||
@@ -184,21 +185,6 @@ class IntentCache:
|
||||
self.cache.clear()
|
||||
|
||||
|
||||
def _get_language_variations(language: str) -> Iterable[str]:
|
||||
"""Generate language codes with and without region."""
|
||||
yield language
|
||||
|
||||
parts = re.split(r"([-_])", language)
|
||||
if len(parts) == 3:
|
||||
lang, sep, region = parts
|
||||
if sep == "_":
|
||||
# en_US -> en-US
|
||||
yield f"{lang}-{region}"
|
||||
|
||||
# en-US -> en
|
||||
yield lang
|
||||
|
||||
|
||||
async def async_setup_default_agent(
|
||||
hass: core.HomeAssistant,
|
||||
entity_component: EntityComponent[ConversationEntity],
|
||||
@@ -914,26 +900,20 @@ class DefaultAgent(ConversationEntity):
|
||||
def _load_intents(self, language: str) -> LanguageIntents | None:
|
||||
"""Load all intents for language (run inside executor)."""
|
||||
intents_dict: dict[str, Any] = {}
|
||||
language_variant: str | None = None
|
||||
supported_langs = set(get_languages())
|
||||
|
||||
# Choose a language variant upfront and commit to it for custom
|
||||
# sentences, etc.
|
||||
all_language_variants = {lang.lower(): lang for lang in supported_langs}
|
||||
lang_matches = language_util.matches(language, supported_langs)
|
||||
|
||||
# en-US, en_US, en, ...
|
||||
for maybe_variant in _get_language_variations(language):
|
||||
matching_variant = all_language_variants.get(maybe_variant.lower())
|
||||
if matching_variant:
|
||||
language_variant = matching_variant
|
||||
break
|
||||
|
||||
if not language_variant:
|
||||
if not lang_matches:
|
||||
_LOGGER.warning(
|
||||
"Unable to find supported language variant for %s", language
|
||||
)
|
||||
return None
|
||||
|
||||
language_variant = lang_matches[0]
|
||||
|
||||
# Load intents for this language variant
|
||||
lang_variant_intents = get_intents(language_variant, json_load=json_load)
|
||||
|
||||
@@ -1329,6 +1309,8 @@ class DefaultAgent(ConversationEntity):
|
||||
async def async_handle_intents(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
) -> intent.IntentResponse | None:
|
||||
"""Try to match sentence against registered intents and return response.
|
||||
|
||||
@@ -1336,7 +1318,9 @@ class DefaultAgent(ConversationEntity):
|
||||
Returns None if no match or a matching error occurred.
|
||||
"""
|
||||
result = await self.async_recognize_intent(user_input, strict_intents_only=True)
|
||||
if not isinstance(result, RecognizeResult):
|
||||
if not isinstance(result, RecognizeResult) or (
|
||||
intent_filter is not None and intent_filter(result)
|
||||
):
|
||||
# No error message on failed match
|
||||
return None
|
||||
|
||||
|
||||
@@ -30,10 +30,15 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
DemoWaterHeater(
|
||||
"Demo Water Heater", 119, UnitOfTemperature.FAHRENHEIT, False, "eco"
|
||||
"Demo Water Heater", 119, UnitOfTemperature.FAHRENHEIT, False, "eco", 1
|
||||
),
|
||||
DemoWaterHeater(
|
||||
"Demo Water Heater Celsius", 45, UnitOfTemperature.CELSIUS, True, "eco"
|
||||
"Demo Water Heater Celsius",
|
||||
45,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
True,
|
||||
"eco",
|
||||
1,
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -52,6 +57,7 @@ class DemoWaterHeater(WaterHeaterEntity):
|
||||
unit_of_measurement: str,
|
||||
away: bool,
|
||||
current_operation: str,
|
||||
target_temperature_step: float,
|
||||
) -> None:
|
||||
"""Initialize the water_heater device."""
|
||||
self._attr_name = name
|
||||
@@ -74,6 +80,7 @@ class DemoWaterHeater(WaterHeaterEntity):
|
||||
"gas",
|
||||
"off",
|
||||
]
|
||||
self._attr_target_temperature_step = target_temperature_step
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperatures."""
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.1.0",
|
||||
"aiodiscover==2.6.0",
|
||||
"cached-ipaddress==0.8.0"
|
||||
"aiodhcpwatcher==1.1.1",
|
||||
"aiodiscover==2.6.1",
|
||||
"cached-ipaddress==0.8.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
MODE_AUTO,
|
||||
HumidifierAction,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityFeature,
|
||||
@@ -41,6 +43,12 @@ async def async_setup_entry(
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
ECOBEE_HUMIDIFIER_ACTION_TO_HASS = {
|
||||
"humidifier": HumidifierAction.HUMIDIFYING,
|
||||
"dehumidifier": HumidifierAction.DRYING,
|
||||
}
|
||||
|
||||
|
||||
class EcobeeHumidifier(HumidifierEntity):
|
||||
"""A humidifier class for an ecobee thermostat with humidifier attached."""
|
||||
|
||||
@@ -52,7 +60,7 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, data, thermostat_index):
|
||||
def __init__(self, data, thermostat_index) -> None:
|
||||
"""Initialize ecobee humidifier platform."""
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
@@ -80,11 +88,11 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return if device is available."""
|
||||
return self.thermostat["runtime"]["connected"]
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self.update_without_throttle:
|
||||
await self.data.update(no_throttle=True)
|
||||
@@ -96,12 +104,20 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
self._last_humidifier_on_mode = self.mode
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def action(self) -> HumidifierAction:
|
||||
"""Return the current action."""
|
||||
for status in self.thermostat["equipmentStatus"].split(","):
|
||||
if status in ECOBEE_HUMIDIFIER_ACTION_TO_HASS:
|
||||
return ECOBEE_HUMIDIFIER_ACTION_TO_HASS[status]
|
||||
return HumidifierAction.IDLE if self.is_on else HumidifierAction.OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the humidifier is on."""
|
||||
return self.mode != MODE_OFF
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
def mode(self) -> str:
|
||||
"""Return the current mode, e.g., off, auto, manual."""
|
||||
return self.thermostat["settings"]["humidifierMode"]
|
||||
|
||||
@@ -118,9 +134,11 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def set_mode(self, mode):
|
||||
def set_mode(self, mode: str) -> None:
|
||||
"""Set humidifier mode (auto, off, manual)."""
|
||||
if mode.lower() not in (self.available_modes):
|
||||
if self.available_modes is None:
|
||||
raise NotImplementedError("Humidifier does not support modes.")
|
||||
if mode.lower() not in self.available_modes:
|
||||
raise ValueError(
|
||||
f"Invalid mode value: {mode} Valid values are"
|
||||
f" {', '.join(self.available_modes)}."
|
||||
@@ -134,10 +152,10 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Set humidifier to off mode."""
|
||||
self.set_mode(MODE_OFF)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set humidifier to on mode."""
|
||||
self.set_mode(self._last_humidifier_on_mode)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"],
|
||||
"requirements": ["pyeconet==0.1.27"]
|
||||
"requirements": ["pyeconet==0.1.28"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.1.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
"message": "Params are required for the command: {command}"
|
||||
},
|
||||
"vacuum_raw_get_positions_not_supported": {
|
||||
"message": "Getting the positions of the chargers and the device itself is not supported"
|
||||
"message": "Retrieving the positions of the chargers and the device itself is not supported"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -264,7 +264,7 @@
|
||||
"services": {
|
||||
"raw_get_positions": {
|
||||
"name": "Get raw positions",
|
||||
"description": "Get the raw response for the positions of the chargers and the device itself."
|
||||
"description": "Retrieves a raw response containing the positions of the chargers and the device itself."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ ECOWITT_SENSORS_MAPPING: Final = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.DEGREE: SensorEntityDescription(
|
||||
key="DEGREE", native_unit_of_measurement=DEGREE
|
||||
key="DEGREE",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
|
||||
key="WATT_METERS_SQUARED",
|
||||
|
||||
@@ -40,12 +40,10 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: str | dict[str, EheimDigitalDevice],
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the climate entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalHeaterClimate] = []
|
||||
if isinstance(device_address, str):
|
||||
device_address = {device_address: coordinator.hub.devices[device_address]}
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalHeater):
|
||||
entities.append(EheimDigitalHeaterClimate(coordinator, device))
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
|
||||
from aiohttp import ClientError
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.hub import EheimDigitalHub
|
||||
from eheimdigital.types import EheimDeviceType
|
||||
from eheimdigital.types import EheimDeviceType, EheimDigitalClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
type AsyncSetupDeviceEntitiesCallback = Callable[
|
||||
[str | dict[str, EheimDigitalDevice]], None
|
||||
]
|
||||
type AsyncSetupDeviceEntitiesCallback = Callable[[dict[str, EheimDigitalDevice]], None]
|
||||
|
||||
type EheimDigitalConfigEntry = ConfigEntry[EheimDigitalUpdateCoordinator]
|
||||
|
||||
@@ -43,12 +43,14 @@ class EheimDigitalUpdateCoordinator(
|
||||
name=DOMAIN,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
self.main_device_added_event = asyncio.Event()
|
||||
self.hub = EheimDigitalHub(
|
||||
host=self.config_entry.data[CONF_HOST],
|
||||
session=async_get_clientsession(hass),
|
||||
loop=hass.loop,
|
||||
receive_callback=self._async_receive_callback,
|
||||
device_found_callback=self._async_device_found,
|
||||
main_device_added_event=self.main_device_added_event,
|
||||
)
|
||||
self.known_devices: set[str] = set()
|
||||
self.platform_callbacks: set[AsyncSetupDeviceEntitiesCallback] = set()
|
||||
@@ -70,14 +72,23 @@ class EheimDigitalUpdateCoordinator(
|
||||
|
||||
if device_address not in self.known_devices:
|
||||
for platform_callback in self.platform_callbacks:
|
||||
platform_callback(device_address)
|
||||
platform_callback({device_address: self.hub.devices[device_address]})
|
||||
|
||||
async def _async_receive_callback(self) -> None:
|
||||
self.async_set_updated_data(self.hub.devices)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
await self.hub.connect()
|
||||
await self.hub.update()
|
||||
try:
|
||||
await self.hub.connect()
|
||||
async with asyncio.timeout(2):
|
||||
# This event gets triggered when the first message is received from
|
||||
# the device, it contains the data necessary to create the main device.
|
||||
# This removes the race condition where the main device is accessed
|
||||
# before the response from the device is parsed.
|
||||
await self.main_device_added_event.wait()
|
||||
await self.hub.update()
|
||||
except (TimeoutError, EheimDigitalClientError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def _async_update_data(self) -> dict[str, EheimDigitalDevice]:
|
||||
try:
|
||||
|
||||
@@ -38,12 +38,10 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: str | dict[str, EheimDigitalDevice],
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the light entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalClassicLEDControlLight] = []
|
||||
if isinstance(device_address, str):
|
||||
device_address = {device_address: coordinator.hub.devices[device_address]}
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicLEDControl):
|
||||
for channel in range(2):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -28,11 +28,13 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"hop_power_savings": {
|
||||
"default": "mdi:percent"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"hop_selector": {
|
||||
"default": "mdi:lightning-bolt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["electrickiwi-api==0.9.14"]
|
||||
}
|
||||
|
||||
105
homeassistant/components/electric_kiwi/quality_scale.yaml
Normal file
105
homeassistant/components/electric_kiwi/quality_scale.yaml
Normal file
@@ -0,0 +1,105 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Does not subscribe to event explicitly.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
Has no options flow
|
||||
docs-installation-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
Handled by OAuth flow (HA is only one with credentials, users cannot get them)
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Web services only
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
Web services only
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
Web services only
|
||||
docs-data-update: todo
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
No devices
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
No devices
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
No unnecessary or noisy entities
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
Handled by OAuth
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
Does not have any repairs
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Does not have devices
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -13,6 +13,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import ATTRIBUTION
|
||||
from .coordinator import ElectricKiwiConfigEntry, ElectricKiwiHOPDataCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_EK_HOP_SELECT = "hop_select"
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ from .coordinator import (
|
||||
ElectricKiwiHOPDataCoordinator,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
ATTR_EK_HOP_START = "hop_power_start"
|
||||
ATTR_EK_HOP_END = "hop_power_end"
|
||||
ATTR_TOTAL_RUNNING_BALANCE = "total_running_balance"
|
||||
|
||||
@@ -498,7 +498,11 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle device found via zeroconf."""
|
||||
host = discovery_info.host
|
||||
host = (
|
||||
f"[{discovery_info.ip_address}]"
|
||||
if discovery_info.ip_address.version == 6
|
||||
else str(discovery_info.ip_address)
|
||||
)
|
||||
https_port = (
|
||||
int(discovery_info.port)
|
||||
if discovery_info.port is not None
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"metering_point_id_already_configured": "Metering point with ID `{metering_point_id}` is already configured.",
|
||||
"no_metering_points": "The provived API token has no metering points."
|
||||
"no_metering_points": "The provided API token has no metering points."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"api_error": "An error occured in the pyemoncms API : {details}"
|
||||
"api_error": "An error occurred in the pyemoncms API : {details}"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.4"]
|
||||
"requirements": ["sense-energy==0.13.5"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class EnOceanEntity(Entity):
|
||||
"""Initialize the device."""
|
||||
self.dev_id = dev_id
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
from .entity import EnvoyBaseEntity
|
||||
from .entity import EnvoyBaseEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -132,6 +132,7 @@ class EnvoyRelayNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
self.data.dry_contact_settings[self._relay_id]
|
||||
)
|
||||
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the relay."""
|
||||
await self.envoy.update_dry_contact(
|
||||
@@ -185,6 +186,7 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
assert self.data.tariff.storage_settings is not None
|
||||
return self.entity_description.value_fn(self.data.tariff.storage_settings)
|
||||
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the storage setting."""
|
||||
await self.entity_description.update_fn(self.envoy, value)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"init": {
|
||||
"title": "Envoy {serial} {host} options",
|
||||
"data": {
|
||||
"diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activies. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again.",
|
||||
"diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activities. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again.",
|
||||
"disable_keep_alive": "Always use a new connection when requesting data from the Envoy. May resolve communication issues with some Envoy firmwares."
|
||||
},
|
||||
"data_description": {
|
||||
|
||||
@@ -5,3 +5,4 @@ ATTR_STATION = "station"
|
||||
CONF_STATION = "station"
|
||||
CONF_TITLE = "title"
|
||||
DOMAIN = "environment_canada"
|
||||
SERVICE_ENVIRONMENT_CANADA_FORECASTS = "get_forecasts"
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"services": {
|
||||
"set_radar_type": {
|
||||
"service": "mdi:radar"
|
||||
},
|
||||
"get_forecasts": {
|
||||
"service": "mdi:weather-cloudy-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
translation_key="wind_bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
value_fn=lambda data: data.conditions.get("wind_bearing", {}).get("value"),
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="wind_chill",
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
get_forecasts:
|
||||
target:
|
||||
entity:
|
||||
integration: environment_canada
|
||||
domain: weather
|
||||
|
||||
set_radar_type:
|
||||
target:
|
||||
entity:
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_forecasts": {
|
||||
"name": "Get forecasts",
|
||||
"description": "Retrieves the forecast from selected weather services."
|
||||
},
|
||||
"set_radar_type": {
|
||||
"name": "Set radar type",
|
||||
"description": "Sets the type of radar image to retrieve.",
|
||||
|
||||
@@ -35,11 +35,16 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SERVICE_ENVIRONMENT_CANADA_FORECASTS
|
||||
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
|
||||
|
||||
# Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/
|
||||
@@ -78,6 +83,14 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities([ECWeatherEntity(config_entry.runtime_data.weather_coordinator)])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ENVIRONMENT_CANADA_FORECASTS,
|
||||
None,
|
||||
"_async_environment_canada_forecasts",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str:
|
||||
"""Calculate unique ID."""
|
||||
@@ -185,6 +198,23 @@ class ECWeatherEntity(
|
||||
"""Return the hourly forecast in native units."""
|
||||
return get_forecast(self.ec_data, True)
|
||||
|
||||
def _async_environment_canada_forecasts(self) -> ServiceResponse:
|
||||
"""Return the native Environment Canada forecast."""
|
||||
daily = []
|
||||
for f in self.ec_data.daily_forecasts:
|
||||
day = f.copy()
|
||||
day["timestamp"] = day["timestamp"].isoformat()
|
||||
daily.append(day)
|
||||
|
||||
hourly = []
|
||||
for f in self.ec_data.hourly_forecasts:
|
||||
hour = f.copy()
|
||||
hour["timestamp"] = hour["period"].isoformat()
|
||||
del hour["period"]
|
||||
hourly.append(hour)
|
||||
|
||||
return {"daily_forecast": daily, "hourly_forecast": hourly}
|
||||
|
||||
|
||||
def get_forecast(ec_data, hourly) -> list[Forecast] | None:
|
||||
"""Build the forecast array."""
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -108,8 +108,7 @@ class ESPHomeDashboardManager:
|
||||
|
||||
reloads = [
|
||||
hass.config_entries.async_reload(entry.entry_id)
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state is ConfigEntryState.LOADED
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
]
|
||||
# Re-auth flows will check the dashboard for encryption key when the form is requested
|
||||
# but we only trigger reauth if the dashboard is available.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.0.0",
|
||||
"aioesphomeapi==29.1.1",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.7.1"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
@@ -40,11 +41,10 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_DURATION_DAYS,
|
||||
ATTR_DURATION_HOURS,
|
||||
ATTR_DURATION,
|
||||
ATTR_DURATION_UNTIL,
|
||||
ATTR_SYSTEM_MODE,
|
||||
ATTR_ZONE_TEMP,
|
||||
ATTR_PERIOD,
|
||||
ATTR_SETPOINT,
|
||||
CONF_LOCATION_IDX,
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL_DEFAULT,
|
||||
@@ -81,7 +81,7 @@ RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_ZONE_TEMP): vol.All(
|
||||
vol.Required(ATTR_SETPOINT): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
|
||||
),
|
||||
vol.Optional(ATTR_DURATION_UNTIL): vol.All(
|
||||
@@ -222,7 +222,7 @@ def setup_service_functions(
|
||||
# Permanent-only modes will use this schema
|
||||
perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]]
|
||||
if perm_modes: # any of: "Auto", "HeatingOff": permanent only
|
||||
schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)})
|
||||
schema = vol.Schema({vol.Required(ATTR_MODE): vol.In(perm_modes)})
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]]
|
||||
@@ -232,8 +232,8 @@ def setup_service_functions(
|
||||
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_HOURS): vol.All(
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
|
||||
),
|
||||
@@ -246,8 +246,8 @@ def setup_service_functions(
|
||||
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_DAYS): vol.All(
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_PERIOD): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_MODE, PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -38,11 +38,10 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import EVOHOME_KEY
|
||||
from .const import (
|
||||
ATTR_DURATION_DAYS,
|
||||
ATTR_DURATION_HOURS,
|
||||
ATTR_DURATION,
|
||||
ATTR_DURATION_UNTIL,
|
||||
ATTR_SYSTEM_MODE,
|
||||
ATTR_ZONE_TEMP,
|
||||
ATTR_PERIOD,
|
||||
ATTR_SETPOINT,
|
||||
EvoService,
|
||||
)
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
@@ -180,7 +179,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
return
|
||||
|
||||
# otherwise it is EvoService.SET_ZONE_OVERRIDE
|
||||
temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp)
|
||||
temperature = max(min(data[ATTR_SETPOINT], self.max_temp), self.min_temp)
|
||||
|
||||
if ATTR_DURATION_UNTIL in data:
|
||||
duration: timedelta = data[ATTR_DURATION_UNTIL]
|
||||
@@ -349,16 +348,16 @@ class EvoController(EvoClimateEntity):
|
||||
Data validation is not required, it will have been done upstream.
|
||||
"""
|
||||
if service == EvoService.SET_SYSTEM_MODE:
|
||||
mode = data[ATTR_SYSTEM_MODE]
|
||||
mode = data[ATTR_MODE]
|
||||
else: # otherwise it is EvoService.RESET_SYSTEM
|
||||
mode = EvoSystemMode.AUTO_WITH_RESET
|
||||
|
||||
if ATTR_DURATION_DAYS in data:
|
||||
if ATTR_PERIOD in data:
|
||||
until = dt_util.start_of_local_day()
|
||||
until += data[ATTR_DURATION_DAYS]
|
||||
until += data[ATTR_PERIOD]
|
||||
|
||||
elif ATTR_DURATION_HOURS in data:
|
||||
until = dt_util.now() + data[ATTR_DURATION_HOURS]
|
||||
elif ATTR_DURATION in data:
|
||||
until = dt_util.now() + data[ATTR_DURATION]
|
||||
|
||||
else:
|
||||
until = None
|
||||
|
||||
@@ -18,11 +18,10 @@ USER_DATA: Final = "user_data"
|
||||
SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60)
|
||||
|
||||
ATTR_SYSTEM_MODE: Final = "mode"
|
||||
ATTR_DURATION_DAYS: Final = "period"
|
||||
ATTR_DURATION_HOURS: Final = "duration"
|
||||
ATTR_PERIOD: Final = "period" # number of days
|
||||
ATTR_DURATION: Final = "duration" # number of minutes, <24h
|
||||
|
||||
ATTR_ZONE_TEMP: Final = "setpoint"
|
||||
ATTR_SETPOINT: Final = "setpoint"
|
||||
ATTR_DURATION_UNTIL: Final = "duration"
|
||||
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
},
|
||||
"period": {
|
||||
"name": "Period",
|
||||
"description": "A period of time in days; used only with Away, DayOff, or Custom. The system will revert to Auto at midnight (up to 99 days, today is day 1)."
|
||||
"description": "A period of time in days; used only with Away, DayOff, or Custom mode. The system will revert to Auto mode at midnight (up to 99 days, today is day 1)."
|
||||
},
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "The duration in hours; used only with AutoWithEco (up to 24 hours)."
|
||||
"description": "The duration in hours; used only with AutoWithEco mode (up to 24 hours)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"reset_system": {
|
||||
"name": "Reset system",
|
||||
"description": "Sets the system to Auto mode and reset all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode)."
|
||||
"description": "Sets the system to Auto mode and resets all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode)."
|
||||
},
|
||||
"refresh_system": {
|
||||
"name": "Refresh system",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"issues": {
|
||||
"import_yaml_error_url_error": {
|
||||
"title": "The Feedreader YAML configuration import failed",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessible for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class FlexitBinarySensor(FlexitEntity, BinarySensorEntity):
|
||||
"""Representation of a Flexit binary Sensor."""
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
MAX_TEMP,
|
||||
MIN_TEMP,
|
||||
PRESET_TO_VENTILATION_MODE_MAP,
|
||||
@@ -43,6 +44,9 @@ async def async_setup_entry(
|
||||
async_add_entities([FlexitClimateEntity(config_entry.runtime_data)])
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
"""Flexit air handling unit."""
|
||||
|
||||
@@ -76,10 +80,6 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.device.serial_number
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh unit state."""
|
||||
await self.device.update()
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return current HVAC action."""
|
||||
@@ -111,7 +111,13 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
else:
|
||||
await self.device.set_air_temp_setpoint_home(temperature)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_temperature",
|
||||
translation_placeholders={
|
||||
"temperature": str(temperature),
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -130,7 +136,13 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
try:
|
||||
await self.device.set_ventilation_mode(ventilation_mode)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_preset_mode",
|
||||
translation_placeholders={
|
||||
"preset": str(ventilation_mode),
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -150,6 +162,12 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
else:
|
||||
await self.device.set_ventilation_mode(VENTILATION_MODE_HOME)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_hvac_mode",
|
||||
translation_placeholders={
|
||||
"mode": str(hvac_mode),
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -49,7 +49,11 @@ class FlexitCoordinator(DataUpdateCoordinator[FlexitBACnet]):
|
||||
await self.device.update()
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting to {self.config_entry.data[CONF_IP_ADDRESS]}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_ready",
|
||||
translation_placeholders={
|
||||
"ip": str(self.config_entry.data[CONF_IP_ADDRESS]),
|
||||
},
|
||||
) from exc
|
||||
|
||||
return self.device
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["flexit_bacnet==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FlexitConfigEntry, FlexitCoordinator
|
||||
from .entity import FlexitEntity
|
||||
|
||||
@@ -205,6 +206,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class FlexitNumber(FlexitEntity, NumberEntity):
|
||||
"""Representation of a Flexit Number."""
|
||||
|
||||
@@ -246,6 +250,12 @@ class FlexitNumber(FlexitEntity, NumberEntity):
|
||||
try:
|
||||
await set_native_value_fn(int(value))
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_value_error",
|
||||
translation_placeholders={
|
||||
"value": str(value),
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
91
homeassistant/components/flexit_bacnet/quality_scale.yaml
Normal file
91
homeassistant/components/flexit_bacnet/quality_scale.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not define custom actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities don't subscribe to events explicitly
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with `await coordinator.async_config_entry_first_refresh()`.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not use options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with coordinator.
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with coordinator.
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration doesn't require any form of authentication.
|
||||
test-coverage: done
|
||||
# Gold
|
||||
entity-translations: done
|
||||
entity-device-class: done
|
||||
devices: done
|
||||
entity-category: todo
|
||||
entity-disabled-by-default: todo
|
||||
discovery: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
diagnostics: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
discovery-update-info: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is not applicable for this integration.
|
||||
docs-use-cases: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-data-update: done
|
||||
docs-known-limitations: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-examples: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: done
|
||||
@@ -161,6 +161,10 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class FlexitSensor(FlexitEntity, SensorEntity):
|
||||
"""Representation of a Flexit (bacnet) Sensor."""
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"device_id": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "The IP address of the Flexit Nordic device",
|
||||
"device_id": "The device ID of the Flexit Nordic device"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -115,5 +119,25 @@
|
||||
"name": "Cooker hood mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"set_value_error": {
|
||||
"message": "Failed setting the value {value}."
|
||||
},
|
||||
"switch_turn": {
|
||||
"message": "Failed to turn the switch {state}."
|
||||
},
|
||||
"set_preset_mode": {
|
||||
"message": "Failed to set preset mode {preset}."
|
||||
},
|
||||
"set_temperature": {
|
||||
"message": "Failed to set temperature {temperature}."
|
||||
},
|
||||
"set_hvac_mode": {
|
||||
"message": "Failed to set HVAC mode {mode}."
|
||||
},
|
||||
"not_ready": {
|
||||
"message": "Timeout while connecting to {ip}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FlexitConfigEntry, FlexitCoordinator
|
||||
from .entity import FlexitEntity
|
||||
|
||||
@@ -68,6 +69,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class FlexitSwitch(FlexitEntity, SwitchEntity):
|
||||
"""Representation of a Flexit Switch."""
|
||||
|
||||
@@ -94,19 +98,31 @@ class FlexitSwitch(FlexitEntity, SwitchEntity):
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn electric heater on."""
|
||||
"""Turn switch on."""
|
||||
try:
|
||||
await self.entity_description.turn_on_fn(self.coordinator.data)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_turn",
|
||||
translation_placeholders={
|
||||
"state": "on",
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn electric heater off."""
|
||||
"""Turn switch off."""
|
||||
try:
|
||||
await self.entity_description.turn_off_fn(self.coordinator.data)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_turn",
|
||||
translation_placeholders={
|
||||
"state": "off",
|
||||
},
|
||||
) from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -45,10 +45,10 @@ class FloEntity(Entity):
|
||||
"""Return True if device is available."""
|
||||
return self._device.available
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""Update Flo entity."""
|
||||
await self._device.async_request_refresh()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(self._device.async_add_listener(self.async_write_ha_state))
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"services": {
|
||||
"ptz": {
|
||||
"name": "PTZ",
|
||||
"description": "Pan/Tilt action for Foscam camera.",
|
||||
"description": "Moves a Foscam camera to a specified direction.",
|
||||
"fields": {
|
||||
"movement": {
|
||||
"name": "Movement",
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
"ptz_preset": {
|
||||
"name": "PTZ preset",
|
||||
"description": "PTZ Preset action for Foscam camera.",
|
||||
"description": "Moves a Foscam camera to a predefined position.",
|
||||
"fields": {
|
||||
"preset_name": {
|
||||
"name": "Preset name",
|
||||
|
||||
@@ -196,6 +196,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.mesh_role = MeshRoles.NONE
|
||||
self.mesh_wifi_uplink = False
|
||||
self.device_conn_type: str | None = None
|
||||
self.device_is_router: bool = False
|
||||
self.password = password
|
||||
@@ -610,6 +611,12 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
ssid=interf.get("ssid", ""),
|
||||
type=interf["type"],
|
||||
)
|
||||
|
||||
if interf["type"].lower() == "wlan" and interf[
|
||||
"name"
|
||||
].lower().startswith("uplink"):
|
||||
self.mesh_wifi_uplink = True
|
||||
|
||||
if dr.format_mac(int_mac) == self.mac:
|
||||
self.mesh_role = MeshRoles(node["mesh_role"])
|
||||
|
||||
|
||||
@@ -207,8 +207,9 @@ async def async_all_entities_list(
|
||||
local_ip: str,
|
||||
) -> list[Entity]:
|
||||
"""Get a list of all entities."""
|
||||
|
||||
if avm_wrapper.mesh_role == MeshRoles.SLAVE:
|
||||
if not avm_wrapper.mesh_wifi_uplink:
|
||||
return [*await _async_wifi_entities_list(avm_wrapper, device_friendly_name)]
|
||||
return []
|
||||
|
||||
return [
|
||||
@@ -565,6 +566,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
avm_wrapper.mesh_role is not MeshRoles.SLAVE
|
||||
)
|
||||
self._network_num = network_num
|
||||
|
||||
switch_info = SwitchInfo(
|
||||
|
||||
@@ -85,6 +85,8 @@ async def async_setup_entry(
|
||||
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
"""The thermostat class for FRITZ!SmartHome thermostats."""
|
||||
|
||||
_attr_max_temp = MAX_TEMPERATURE
|
||||
_attr_min_temp = MIN_TEMPERATURE
|
||||
_attr_precision = PRECISION_HALVES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = "thermostat"
|
||||
@@ -135,11 +137,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
elif target_temp is not None:
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
if target_temp == OFF_API_TEMPERATURE:
|
||||
target_temp = OFF_REPORT_SET_TEMPERATURE
|
||||
elif target_temp == ON_API_TEMPERATURE:
|
||||
target_temp = ON_REPORT_SET_TEMPERATURE
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.set_target_temperature, target_temp, True
|
||||
)
|
||||
@@ -169,12 +173,12 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_hvac_while_active_mode",
|
||||
)
|
||||
if self.hvac_mode == hvac_mode:
|
||||
if self.hvac_mode is hvac_mode:
|
||||
LOGGER.debug(
|
||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||
)
|
||||
return
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if hvac_mode is HVACMode.OFF:
|
||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
else:
|
||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||
@@ -208,16 +212,6 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> int:
|
||||
"""Return the minimum temperature."""
|
||||
return MIN_TEMPERATURE
|
||||
|
||||
@property
|
||||
def max_temp(self) -> int:
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMPERATURE
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
"""Return the device specific state attributes."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user