Compare commits
1 commit
dev
...
disable-in
Author | SHA1 | Date | |
---|---|---|---|
|
a1fa495bb6 |
222
.core_files.yaml
|
@ -3,121 +3,102 @@
|
||||||
core: &core
|
core: &core
|
||||||
- homeassistant/*.py
|
- homeassistant/*.py
|
||||||
- homeassistant/auth/**
|
- homeassistant/auth/**
|
||||||
- homeassistant/helpers/**
|
- homeassistant/helpers/*
|
||||||
- homeassistant/package_constraints.txt
|
- homeassistant/package_constraints.txt
|
||||||
- homeassistant/util/**
|
- homeassistant/util/*
|
||||||
- pyproject.toml
|
- pyproject.toml
|
||||||
- requirements.txt
|
- requirements.txt
|
||||||
- setup.cfg
|
- setup.cfg
|
||||||
|
|
||||||
# Our base platforms, that are used by other integrations
|
# Our base platforms, that are used by other integrations
|
||||||
base_platforms: &base_platforms
|
base_platforms: &base_platforms
|
||||||
- homeassistant/components/air_quality/**
|
- homeassistant/components/air_quality/*
|
||||||
- homeassistant/components/alarm_control_panel/**
|
- homeassistant/components/alarm_control_panel/*
|
||||||
- homeassistant/components/assist_satellite/**
|
- homeassistant/components/binary_sensor/*
|
||||||
- homeassistant/components/binary_sensor/**
|
- homeassistant/components/button/*
|
||||||
- homeassistant/components/button/**
|
- homeassistant/components/calendar/*
|
||||||
- homeassistant/components/calendar/**
|
- homeassistant/components/camera/*
|
||||||
- homeassistant/components/camera/**
|
- homeassistant/components/climate/*
|
||||||
- homeassistant/components/climate/**
|
- homeassistant/components/cover/*
|
||||||
- homeassistant/components/cover/**
|
- homeassistant/components/device_tracker/*
|
||||||
- homeassistant/components/date/**
|
- homeassistant/components/diagnostics/*
|
||||||
- homeassistant/components/datetime/**
|
- homeassistant/components/fan/*
|
||||||
- homeassistant/components/device_tracker/**
|
- homeassistant/components/geo_location/*
|
||||||
- homeassistant/components/diagnostics/**
|
- homeassistant/components/humidifier/*
|
||||||
- homeassistant/components/event/**
|
- homeassistant/components/image_processing/*
|
||||||
- homeassistant/components/fan/**
|
- homeassistant/components/light/*
|
||||||
- homeassistant/components/geo_location/**
|
- homeassistant/components/lock/*
|
||||||
- homeassistant/components/humidifier/**
|
- homeassistant/components/media_player/*
|
||||||
- homeassistant/components/image/**
|
- homeassistant/components/notify/*
|
||||||
- homeassistant/components/image_processing/**
|
- homeassistant/components/number/*
|
||||||
- homeassistant/components/lawn_mower/**
|
- homeassistant/components/remote/*
|
||||||
- homeassistant/components/light/**
|
- homeassistant/components/scene/*
|
||||||
- homeassistant/components/lock/**
|
- homeassistant/components/select/*
|
||||||
- homeassistant/components/media_player/**
|
- homeassistant/components/sensor/*
|
||||||
- homeassistant/components/notify/**
|
- homeassistant/components/siren/*
|
||||||
- homeassistant/components/number/**
|
- homeassistant/components/stt/*
|
||||||
- homeassistant/components/remote/**
|
- homeassistant/components/switch/*
|
||||||
- homeassistant/components/scene/**
|
- homeassistant/components/tts/*
|
||||||
- homeassistant/components/select/**
|
- homeassistant/components/vacuum/*
|
||||||
- homeassistant/components/sensor/**
|
- homeassistant/components/water_heater/*
|
||||||
- homeassistant/components/siren/**
|
- homeassistant/components/weather/*
|
||||||
- homeassistant/components/stt/**
|
|
||||||
- homeassistant/components/switch/**
|
|
||||||
- homeassistant/components/text/**
|
|
||||||
- homeassistant/components/time/**
|
|
||||||
- homeassistant/components/todo/**
|
|
||||||
- homeassistant/components/tts/**
|
|
||||||
- homeassistant/components/update/**
|
|
||||||
- homeassistant/components/vacuum/**
|
|
||||||
- homeassistant/components/valve/**
|
|
||||||
- homeassistant/components/water_heater/**
|
|
||||||
- homeassistant/components/weather/**
|
|
||||||
|
|
||||||
# Extra components that trigger the full suite
|
# Extra components that trigger the full suite
|
||||||
components: &components
|
components: &components
|
||||||
- homeassistant/components/alexa/**
|
- homeassistant/components/alert/*
|
||||||
- homeassistant/components/application_credentials/**
|
- homeassistant/components/alexa/*
|
||||||
- homeassistant/components/assist_pipeline/**
|
- homeassistant/components/auth/*
|
||||||
- homeassistant/components/auth/**
|
- homeassistant/components/automation/*
|
||||||
- homeassistant/components/automation/**
|
- homeassistant/components/cloud/*
|
||||||
- homeassistant/components/backup/**
|
- homeassistant/components/config/*
|
||||||
- homeassistant/components/blueprint/**
|
- homeassistant/components/configurator/*
|
||||||
- homeassistant/components/bluetooth/**
|
- homeassistant/components/conversation/*
|
||||||
- homeassistant/components/cloud/**
|
- homeassistant/components/demo/*
|
||||||
- homeassistant/components/config/**
|
- homeassistant/components/device_automation/*
|
||||||
- homeassistant/components/configurator/**
|
- homeassistant/components/dhcp/*
|
||||||
- homeassistant/components/conversation/**
|
- homeassistant/components/discovery/*
|
||||||
- homeassistant/components/demo/**
|
- homeassistant/components/energy/*
|
||||||
- homeassistant/components/device_automation/**
|
- homeassistant/components/ffmpeg/*
|
||||||
- homeassistant/components/dhcp/**
|
- homeassistant/components/frontend/*
|
||||||
- homeassistant/components/discovery/**
|
- homeassistant/components/google_assistant/*
|
||||||
- homeassistant/components/energy/**
|
- homeassistant/components/group/*
|
||||||
- homeassistant/components/ffmpeg/**
|
- homeassistant/components/hassio/*
|
||||||
- homeassistant/components/frontend/**
|
|
||||||
- homeassistant/components/google_assistant/**
|
|
||||||
- homeassistant/components/group/**
|
|
||||||
- homeassistant/components/hassio/**
|
|
||||||
- homeassistant/components/homeassistant/**
|
- homeassistant/components/homeassistant/**
|
||||||
- homeassistant/components/homeassistant_hardware/**
|
|
||||||
- homeassistant/components/http/**
|
- homeassistant/components/http/**
|
||||||
- homeassistant/components/image/**
|
- homeassistant/components/image/*
|
||||||
- homeassistant/components/input_boolean/**
|
- homeassistant/components/input_boolean/*
|
||||||
- homeassistant/components/input_button/**
|
- homeassistant/components/input_button/*
|
||||||
- homeassistant/components/input_datetime/**
|
- homeassistant/components/input_datetime/*
|
||||||
- homeassistant/components/input_number/**
|
- homeassistant/components/input_number/*
|
||||||
- homeassistant/components/input_select/**
|
- homeassistant/components/input_select/*
|
||||||
- homeassistant/components/input_text/**
|
- homeassistant/components/input_text/*
|
||||||
- homeassistant/components/logbook/**
|
- homeassistant/components/logbook/*
|
||||||
- homeassistant/components/logger/**
|
- homeassistant/components/logger/*
|
||||||
- homeassistant/components/lovelace/**
|
- homeassistant/components/lovelace/*
|
||||||
- homeassistant/components/media_source/**
|
- homeassistant/components/media_source/*
|
||||||
- homeassistant/components/mjpeg/**
|
- homeassistant/components/mjpeg/*
|
||||||
- homeassistant/components/modbus/**
|
- homeassistant/components/mqtt/*
|
||||||
- homeassistant/components/mqtt/**
|
- homeassistant/components/network/*
|
||||||
- homeassistant/components/network/**
|
- homeassistant/components/onboarding/*
|
||||||
- homeassistant/components/onboarding/**
|
- homeassistant/components/otp/*
|
||||||
- homeassistant/components/otp/**
|
- homeassistant/components/persistent_notification/*
|
||||||
- homeassistant/components/persistent_notification/**
|
- homeassistant/components/person/*
|
||||||
- homeassistant/components/person/**
|
- homeassistant/components/recorder/*
|
||||||
- homeassistant/components/recorder/**
|
- homeassistant/components/safe_mode/*
|
||||||
- homeassistant/components/recovery_mode/**
|
- homeassistant/components/script/*
|
||||||
- homeassistant/components/repairs/**
|
- homeassistant/components/shopping_list/*
|
||||||
- homeassistant/components/script/**
|
- homeassistant/components/ssdp/*
|
||||||
- homeassistant/components/shopping_list/**
|
- homeassistant/components/stream/*
|
||||||
- homeassistant/components/ssdp/**
|
- homeassistant/components/sun/*
|
||||||
- homeassistant/components/stream/**
|
- homeassistant/components/system_health/*
|
||||||
- homeassistant/components/sun/**
|
- homeassistant/components/tag/*
|
||||||
- homeassistant/components/system_health/**
|
- homeassistant/components/template/*
|
||||||
- homeassistant/components/tag/**
|
- homeassistant/components/timer/*
|
||||||
- homeassistant/components/template/**
|
- homeassistant/components/usb/*
|
||||||
- homeassistant/components/timer/**
|
- homeassistant/components/webhook/*
|
||||||
- homeassistant/components/trace/**
|
- homeassistant/components/websocket_api/*
|
||||||
- homeassistant/components/usb/**
|
- homeassistant/components/zeroconf/*
|
||||||
- homeassistant/components/webhook/**
|
- homeassistant/components/zone/*
|
||||||
- homeassistant/components/websocket_api/**
|
|
||||||
- homeassistant/components/zeroconf/**
|
|
||||||
- homeassistant/components/zone/**
|
|
||||||
|
|
||||||
# Testing related files that affect the whole test/linting suite
|
# Testing related files that affect the whole test/linting suite
|
||||||
tests: &tests
|
tests: &tests
|
||||||
|
@ -125,40 +106,33 @@ tests: &tests
|
||||||
- pylint/**
|
- pylint/**
|
||||||
- requirements_test_pre_commit.txt
|
- requirements_test_pre_commit.txt
|
||||||
- requirements_test.txt
|
- requirements_test.txt
|
||||||
- tests/*.py
|
|
||||||
- tests/auth/**
|
- tests/auth/**
|
||||||
- tests/backports/**
|
- tests/backports/*
|
||||||
- tests/components/conftest.py
|
- tests/common.py
|
||||||
- tests/components/diagnostics/**
|
- tests/conftest.py
|
||||||
- tests/components/history/**
|
- tests/hassfest/*
|
||||||
- tests/components/logbook/**
|
- tests/helpers/*
|
||||||
- tests/components/recorder/**
|
- tests/ignore_uncaught_exceptions.py
|
||||||
- tests/components/repairs/**
|
- tests/mock/*
|
||||||
- tests/components/sensor/**
|
- tests/pylint/*
|
||||||
- tests/hassfest/**
|
- tests/scripts/*
|
||||||
- tests/helpers/**
|
- tests/test_util/*
|
||||||
- tests/mock/**
|
|
||||||
- tests/pylint/**
|
|
||||||
- tests/scripts/**
|
|
||||||
- tests/test_util/**
|
|
||||||
- tests/testing_config/**
|
- tests/testing_config/**
|
||||||
- tests/util/**
|
- tests/util/**
|
||||||
|
|
||||||
other: &other
|
other: &other
|
||||||
- .github/workflows/**
|
- .github/workflows/*
|
||||||
- homeassistant/scripts/**
|
- homeassistant/scripts/**
|
||||||
|
|
||||||
requirements: &requirements
|
requirements:
|
||||||
- .github/workflows/**
|
- .github/workflows/*
|
||||||
- homeassistant/package_constraints.txt
|
- homeassistant/package_constraints.txt
|
||||||
- requirements*.txt
|
- requirements*.txt
|
||||||
- pyproject.toml
|
- setup.py
|
||||||
- script/licenses.py
|
|
||||||
|
|
||||||
any:
|
any:
|
||||||
- *base_platforms
|
- *base_platforms
|
||||||
- *components
|
- *components
|
||||||
- *core
|
- *core
|
||||||
- *other
|
- *other
|
||||||
- *requirements
|
|
||||||
- *tests
|
- *tests
|
||||||
|
|
1517
.coveragerc
Normal file
|
@ -2,42 +2,24 @@
|
||||||
"name": "Home Assistant Dev",
|
"name": "Home Assistant Dev",
|
||||||
"context": "..",
|
"context": "..",
|
||||||
"dockerFile": "../Dockerfile.dev",
|
"dockerFile": "../Dockerfile.dev",
|
||||||
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && script/setup",
|
"postCreateCommand": "script/setup",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postStartCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
"containerEnv": { "DEVCONTAINER": "1" },
|
||||||
"PYTHONASYNCIODEBUG": "1"
|
"appPort": 8123,
|
||||||
},
|
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
||||||
},
|
|
||||||
// Port 5683 udp is used by Shelly integration
|
|
||||||
"appPort": ["8123:8123", "5683:5683/udp"],
|
|
||||||
"runArgs": [
|
|
||||||
"-e",
|
|
||||||
"GIT_EDITOR=code --wait",
|
|
||||||
"--security-opt",
|
|
||||||
"label=disable"
|
|
||||||
],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"charliermarsh.ruff",
|
|
||||||
"ms-python.pylint",
|
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
"visualstudioexptteam.vscodeintellicode",
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode"
|
||||||
"GitHub.vscode-pull-request-github",
|
|
||||||
"GitHub.copilot"
|
|
||||||
],
|
],
|
||||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
"python.pythonPath": "/usr/local/bin/python",
|
||||||
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
"python.linting.pylintEnabled": true,
|
||||||
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
|
"python.linting.enabled": true,
|
||||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
"python.formatting.provider": "black",
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
"pylint.importStrategy": "fromEnvironment",
|
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
|
@ -55,17 +37,6 @@
|
||||||
"!include_dir_list scalar",
|
"!include_dir_list scalar",
|
||||||
"!include_dir_merge_list scalar",
|
"!include_dir_merge_list scalar",
|
||||||
"!include_dir_merge_named scalar"
|
"!include_dir_merge_named scalar"
|
||||||
],
|
|
||||||
"[python]": {
|
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
|
||||||
},
|
|
||||||
"json.schemas": [
|
|
||||||
{
|
|
||||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
|
||||||
"url": "./script/json_schemas/manifest_schema.json"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ docs
|
||||||
# Development
|
# Development
|
||||||
.devcontainer
|
.devcontainer
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
|
||||||
|
|
||||||
# Test related files
|
# Test related files
|
||||||
|
.tox
|
||||||
tests
|
tests
|
||||||
|
|
||||||
# Other virtualization methods
|
# Other virtualization methods
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
# Black
|
|
||||||
4de97abc3aa83188666336ce0a015a5bab75bc8f
|
|
||||||
|
|
||||||
# Switch formatting from black to ruff-format (#102893)
|
|
||||||
706add4a57120a93d7b7fe40e722b00d634c76c2
|
|
||||||
|
|
||||||
# Prettify json (component test fixtures) (#68892)
|
|
||||||
053c4428a933c3c04c22642f93c93fccba3e8bfd
|
|
||||||
|
|
||||||
# Prettify json (tests) (#68888)
|
|
||||||
496d90bf00429d9d924caeb0155edc0bf54e86b9
|
|
||||||
|
|
||||||
# Bump ruff to 0.3.4 (#112690)
|
|
||||||
6bb4e7d62c60389608acf4a7d7dacd8f029307dd
|
|
1
.gitattributes
vendored
|
@ -8,6 +8,5 @@
|
||||||
*.png binary
|
*.png binary
|
||||||
*.zip binary
|
*.zip binary
|
||||||
*.mp3 binary
|
*.mp3 binary
|
||||||
*.pcm binary
|
|
||||||
|
|
||||||
Dockerfile.dev linguist-language=Dockerfile
|
Dockerfile.dev linguist-language=Dockerfile
|
||||||
|
|
3
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
||||||
custom: https://www.openhomefoundation.org
|
custom: https://www.nabucasa.com
|
||||||
|
github: balloob
|
||||||
|
|
2
.github/ISSUE_TEMPLATE.md
vendored
|
@ -16,7 +16,7 @@
|
||||||
<!--
|
<!--
|
||||||
Provide details about the versions you are using, which helps us to reproduce
|
Provide details about the versions you are using, which helps us to reproduce
|
||||||
and find the issue quicker. Version information is found in the
|
and find the issue quicker. Version information is found in the
|
||||||
Home Assistant frontend: Settings -> About.
|
Home Assistant frontend: Configuration -> Info.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- Home Assistant Core release with the issue:
|
- Home Assistant Core release with the issue:
|
||||||
|
|
20
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -31,9 +31,9 @@ body:
|
||||||
label: What version of Home Assistant Core has the issue?
|
label: What version of Home Assistant Core has the issue?
|
||||||
placeholder: core-
|
placeholder: core-
|
||||||
description: >
|
description: >
|
||||||
Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/).
|
Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/).
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/system_health/)
|
[](https://my.home-assistant.io/redirect/info/)
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: What was the last working version of Home Assistant Core?
|
label: What was the last working version of Home Assistant Core?
|
||||||
|
@ -46,9 +46,9 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: What type of installation are you running?
|
label: What type of installation are you running?
|
||||||
description: >
|
description: >
|
||||||
Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/).
|
Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/).
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/system_health/)
|
[](https://my.home-assistant.io/redirect/info/)
|
||||||
options:
|
options:
|
||||||
- Home Assistant OS
|
- Home Assistant OS
|
||||||
- Home Assistant Container
|
- Home Assistant Container
|
||||||
|
@ -59,15 +59,15 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Integration causing the issue
|
label: Integration causing the issue
|
||||||
description: >
|
description: >
|
||||||
The name of the integration, for example Automation or Philips Hue.
|
The name of the integration. For example: Automation, Philips Hue
|
||||||
- type: input
|
- type: input
|
||||||
id: integration_link
|
id: integration_link
|
||||||
attributes:
|
attributes:
|
||||||
label: Link to integration documentation on our website
|
label: Link to integration documentation on our website
|
||||||
placeholder: "https://www.home-assistant.io/integrations/..."
|
placeholder: "https://www.home-assistant.io/integrations/..."
|
||||||
description: |
|
description: |
|
||||||
Providing a link [to the documentation][docs] helps us categorize the issue and might speed up the
|
Providing a link [to the documentation][docs] helps us categorize the
|
||||||
investigation by automatically informing a contributor, while also providing a useful reference for others.
|
issue, while also providing a useful reference for others.
|
||||||
|
|
||||||
[docs]: https://www.home-assistant.io/integrations
|
[docs]: https://www.home-assistant.io/integrations
|
||||||
|
|
||||||
|
@ -78,12 +78,12 @@ body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Diagnostics information
|
label: Diagnostics information
|
||||||
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
|
|
||||||
description: >-
|
description: >-
|
||||||
Many integrations provide the ability to [download diagnostic data](https://www.home-assistant.io/docs/configuration/troubleshooting/#debug-logs-and-diagnostics).
|
Many integrations provide the ability to download diagnostic data
|
||||||
|
on the device page (and on the integration dashboard).
|
||||||
|
|
||||||
**It would really help if you could download the diagnostics data for the device you are having issues with,
|
**It would really help if you could download the diagnostics data for the device you are having issues with,
|
||||||
and <ins>drag-and-drop that file into the textbox below.</ins>**
|
and drag-and-drop that file into the textbox below.**
|
||||||
|
|
||||||
It generally allows pinpointing defects and thus resolving issues faster.
|
It generally allows pinpointing defects and thus resolving issues faster.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report a bug with the UI, Frontend or Dashboards
|
- name: Report a bug with the UI, Frontend or Lovelace
|
||||||
url: https://github.com/home-assistant/frontend/issues
|
url: https://github.com/home-assistant/frontend/issues
|
||||||
about: This is the issue tracker for our backend. Please report issues with the UI in the frontend repository.
|
about: This is the issue tracker for our backend. Please report issues with the UI in the frontend repository.
|
||||||
- name: Report incorrect or missing information on our website
|
- name: Report incorrect or missing information on our website
|
||||||
|
|
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -33,7 +33,6 @@
|
||||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||||
- [ ] New integration (thank you!)
|
- [ ] New integration (thank you!)
|
||||||
- [ ] New feature (which adds functionality to an existing integration)
|
- [ ] New feature (which adds functionality to an existing integration)
|
||||||
- [ ] Deprecation (breaking change to happen in the future)
|
|
||||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||||
- [ ] Code quality improvements to existing code or addition of tests
|
- [ ] Code quality improvements to existing code or addition of tests
|
||||||
|
|
||||||
|
@ -59,8 +58,7 @@
|
||||||
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
||||||
- [ ] There is no commented out code in this PR.
|
- [ ] There is no commented out code in this PR.
|
||||||
- [ ] I have followed the [development checklist][dev-checklist]
|
- [ ] I have followed the [development checklist][dev-checklist]
|
||||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
- [ ] The code has been formatted using Black (`black --fast homeassistant tests`)
|
||||||
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
|
|
||||||
- [ ] Tests have been added to verify that the new code works.
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
|
@ -74,6 +72,19 @@ If the code communicates with devices, web services, or third-party tools:
|
||||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||||
Updated by running `python3 -m script.gen_requirements_all`.
|
Updated by running `python3 -m script.gen_requirements_all`.
|
||||||
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
||||||
|
- [ ] Untested files have been added to `.coveragerc`.
|
||||||
|
|
||||||
|
The integration reached or maintains the following [Integration Quality Scale][quality-scale]:
|
||||||
|
<!--
|
||||||
|
The Integration Quality Scale scores an integration on the code quality
|
||||||
|
and user experience. Each level of the quality scale consists of a list
|
||||||
|
of requirements. We highly recommend getting your integration scored!
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] No score or internal
|
||||||
|
- [ ] 🥈 Silver
|
||||||
|
- [ ] 🥇 Gold
|
||||||
|
- [ ] 🏆 Platinum
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
This project is very active and we have a high turnover of pull requests.
|
This project is very active and we have a high turnover of pull requests.
|
||||||
|
@ -103,8 +114,7 @@ To help with the load of incoming pull requests:
|
||||||
|
|
||||||
Below, some useful links you could explore:
|
Below, some useful links you could explore:
|
||||||
-->
|
-->
|
||||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||||
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
|
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
|
||||||
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
|
[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html
|
||||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
|
||||||
|
|
BIN
.github/assets/screenshot-integrations.png
vendored
Before Width: | Height: | Size: 65 KiB |
BIN
.github/assets/screenshot-states.png
vendored
Before Width: | Height: | Size: 115 KiB |
13
.github/move.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Configuration for move-issues - https://github.com/dessant/move-issues
|
||||||
|
|
||||||
|
# Delete the command comment. Ignored when the comment also contains other content
|
||||||
|
deleteCommand: true
|
||||||
|
# Close the source issue after moving
|
||||||
|
closeSourceIssue: true
|
||||||
|
# Lock the source issue after moving
|
||||||
|
lockSourceIssue: false
|
||||||
|
# Set custom aliases for targets
|
||||||
|
# aliases:
|
||||||
|
# r: repo
|
||||||
|
# or: owner/repo
|
||||||
|
|
423
.github/workflows/builder.yml
vendored
|
@ -10,10 +10,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: core
|
BUILD_TYPE: core
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: 3.9
|
||||||
PIP_TIMEOUT: 60
|
|
||||||
UV_HTTP_TIMEOUT: 60
|
|
||||||
UV_SYSTEM_PYTHON: "true"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
|
@ -27,12 +24,12 @@ jobs:
|
||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v2.3.2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
@ -51,170 +48,109 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ignore-dev: true
|
ignore-dev: true
|
||||||
|
|
||||||
- name: Fail if translations files are checked in
|
- name: Generate meta info
|
||||||
run: |
|
|
||||||
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
|
|
||||||
echo "Translations files are checked in, please remove the following files:"
|
|
||||||
find homeassistant/components/*/translations -type f
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Download Translations
|
|
||||||
run: python3 -m script.translations download
|
|
||||||
env:
|
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
|
||||||
|
|
||||||
- name: Archive translations
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: |
|
||||||
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Signing meta info file
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: home-assistant/actions/helpers/codenotary@master
|
||||||
with:
|
with:
|
||||||
name: translations
|
source: file://${{ github.workspace }}/OFFICIAL_IMAGE
|
||||||
path: translations.tar.gz
|
asset: OFFICIAL_IMAGE-${{ steps.version.outputs.version }}
|
||||||
if-no-files-found: error
|
token: ${{ secrets.CAS_TOKEN }}
|
||||||
|
|
||||||
|
build_python:
|
||||||
|
name: Build PyPi package
|
||||||
|
needs: init
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.3.2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Remove dist, build, and homeassistant.egg-info
|
||||||
|
# when build locally for testing!
|
||||||
|
pip install twine build
|
||||||
|
python -m build
|
||||||
|
|
||||||
|
- name: Upload package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export TWINE_USERNAME="__token__"
|
||||||
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
|
|
||||||
|
twine upload dist/* --skip-existing
|
||||||
|
|
||||||
build_base:
|
build_base:
|
||||||
name: Build ${{ matrix.arch }} base core image
|
name: Build ${{ matrix.arch }} base core image
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
|
||||||
if: needs.init.outputs.channel == 'dev'
|
|
||||||
uses: dawidd6/action-download-artifact@v6
|
|
||||||
with:
|
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
repo: home-assistant/frontend
|
|
||||||
branch: dev
|
|
||||||
workflow: nightly.yaml
|
|
||||||
workflow_conclusion: success
|
|
||||||
name: wheels
|
|
||||||
|
|
||||||
- name: Download nightly wheels of intents
|
|
||||||
if: needs.init.outputs.channel == 'dev'
|
|
||||||
uses: dawidd6/action-download-artifact@v6
|
|
||||||
with:
|
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
repo: home-assistant/intents-package
|
|
||||||
branch: main
|
|
||||||
workflow: nightly.yaml
|
|
||||||
workflow_conclusion: success
|
|
||||||
name: package
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v2.3.2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Adjust nightly version
|
- name: Adjust nightly version
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
UV_PRERELEASE: allow
|
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install "$(grep '^uv' < requirements.txt)"
|
python3 -m pip install packaging
|
||||||
uv pip install packaging tomli
|
python3 -m pip install --use-deprecated=legacy-resolver .
|
||||||
uv pip install .
|
python3 script/version_bump.py nightly
|
||||||
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
version="$(python setup.py -V)"
|
||||||
|
|
||||||
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
|
|
||||||
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}"
|
|
||||||
frontend_version="${BASH_REMATCH[1]}" yq \
|
|
||||||
--inplace e -o json \
|
|
||||||
'.requirements = ["home-assistant-frontend=="+env(frontend_version)]' \
|
|
||||||
homeassistant/components/frontend/manifest.json
|
|
||||||
|
|
||||||
sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \
|
|
||||||
homeassistant/package_constraints.txt
|
|
||||||
|
|
||||||
sed -i "s|home-assistant-frontend==.*||" requirements_all.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then
|
|
||||||
echo "Found intents wheel, setting version to: ${BASH_REMATCH[1]}"
|
|
||||||
yq \
|
|
||||||
--inplace e -o json \
|
|
||||||
'del(.requirements[] | select(contains("home-assistant-intents")))' \
|
|
||||||
homeassistant/components/conversation/manifest.json
|
|
||||||
|
|
||||||
intents_version="${BASH_REMATCH[1]}" yq \
|
|
||||||
--inplace e -o json \
|
|
||||||
'.requirements += ["home-assistant-intents=="+env(intents_version)]' \
|
|
||||||
homeassistant/components/conversation/manifest.json
|
|
||||||
|
|
||||||
sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
|
|
||||||
homeassistant/package_constraints.txt
|
|
||||||
|
|
||||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Adjustments for armhf
|
|
||||||
if: matrix.arch == 'armhf'
|
|
||||||
run: |
|
|
||||||
# Pandas has issues building on armhf, it is expected they
|
|
||||||
# will drop the platform in the near future (they consider it
|
|
||||||
# "flimsy" on 386). The following packages depend on pandas,
|
|
||||||
# so we comment them out.
|
|
||||||
sed -i "s|env-canada|# env-canada|g" requirements_all.txt
|
|
||||||
sed -i "s|noaa-coops|# noaa-coops|g" requirements_all.txt
|
|
||||||
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
|
|
||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
|
||||||
|
|
||||||
- name: Download translations
|
|
||||||
uses: actions/download-artifact@v4.1.8
|
|
||||||
with:
|
|
||||||
name: translations
|
|
||||||
|
|
||||||
- name: Extract translations
|
|
||||||
run: |
|
|
||||||
tar xvf translations.tar.gz
|
|
||||||
rm translations.tar.gz
|
|
||||||
|
|
||||||
- name: Write meta info file
|
- name: Write meta info file
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1.12.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v1.12.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2024.08.2
|
uses: home-assistant/builder@2022.01.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
--${{ matrix.arch }} \
|
--${{ matrix.arch }} \
|
||||||
--cosign \
|
|
||||||
--target /data \
|
--target /data \
|
||||||
--generic ${{ needs.init.outputs.version }}
|
--generic ${{ needs.init.outputs.version }}
|
||||||
|
env:
|
||||||
|
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
||||||
|
|
||||||
build_machine:
|
build_machine:
|
||||||
name: Build ${{ matrix.machine }} machine core image
|
name: Build ${{ matrix.machine }} machine core image
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: ["init", "build_base"]
|
needs: ["init", "build_base"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
machine:
|
machine:
|
||||||
|
@ -223,7 +159,6 @@ jobs:
|
||||||
- khadas-vim3
|
- khadas-vim3
|
||||||
- odroid-c2
|
- odroid-c2
|
||||||
- odroid-c4
|
- odroid-c4
|
||||||
- odroid-m1
|
|
||||||
- odroid-n2
|
- odroid-n2
|
||||||
- odroid-xu
|
- odroid-xu
|
||||||
- qemuarm
|
- qemuarm
|
||||||
|
@ -236,13 +171,10 @@ jobs:
|
||||||
- raspberrypi3-64
|
- raspberrypi3-64
|
||||||
- raspberrypi4
|
- raspberrypi4
|
||||||
- raspberrypi4-64
|
- raspberrypi4-64
|
||||||
- raspberrypi5-64
|
|
||||||
- tinker
|
- tinker
|
||||||
- yellow
|
|
||||||
- green
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
|
@ -255,31 +187,37 @@ jobs:
|
||||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1.12.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v1.12.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2024.08.2
|
uses: home-assistant/builder@2022.01.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
--target /data/machine \
|
--target /data/machine \
|
||||||
--cosign \
|
|
||||||
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
||||||
|
env:
|
||||||
|
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
||||||
|
|
||||||
publish_ha:
|
publish_ha:
|
||||||
name: Publish version files
|
name: Publish version files
|
||||||
environment: ${{ needs.init.outputs.channel }}
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: ["init", "build_machine"]
|
needs: ["init", "build_machine"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
|
@ -306,233 +244,108 @@ jobs:
|
||||||
channel: beta
|
channel: beta
|
||||||
|
|
||||||
publish_container:
|
publish_container:
|
||||||
name: Publish meta container for ${{ matrix.registry }}
|
name: Publish meta container
|
||||||
environment: ${{ needs.init.outputs.channel }}
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: ["init", "build_base"]
|
needs: ["init", "build_base"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
|
||||||
with:
|
|
||||||
cosign-release: "v2.2.3"
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
uses: docker/login-action@v1.12.0
|
||||||
uses: docker/login-action@v3.3.0
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
uses: docker/login-action@v1.12.0
|
||||||
uses: docker/login-action@v3.3.0
|
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install CAS tools
|
||||||
|
uses: home-assistant/actions/helpers/cas@master
|
||||||
|
|
||||||
- name: Build Meta Image
|
- name: Build Meta Image
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
|
||||||
function create_manifest() {
|
function create_manifest() {
|
||||||
local tag_l=${1}
|
local docker_reg=${1}
|
||||||
local tag_r=${2}
|
local tag_l=${2}
|
||||||
local registry=${{ matrix.registry }}
|
local tag_r=${3}
|
||||||
|
|
||||||
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
docker manifest create "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
"${docker_reg}/amd64-homeassistant:${tag_r}" \
|
||||||
"${registry}/i386-homeassistant:${tag_r}" \
|
"${docker_reg}/i386-homeassistant:${tag_r}" \
|
||||||
"${registry}/armhf-homeassistant:${tag_r}" \
|
"${docker_reg}/armhf-homeassistant:${tag_r}" \
|
||||||
"${registry}/armv7-homeassistant:${tag_r}" \
|
"${docker_reg}/armv7-homeassistant:${tag_r}" \
|
||||||
"${registry}/aarch64-homeassistant:${tag_r}"
|
"${docker_reg}/aarch64-homeassistant:${tag_r}"
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
"${docker_reg}/amd64-homeassistant:${tag_r}" \
|
||||||
--os linux --arch amd64
|
--os linux --arch amd64
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/i386-homeassistant:${tag_r}" \
|
"${docker_reg}/i386-homeassistant:${tag_r}" \
|
||||||
--os linux --arch 386
|
--os linux --arch 386
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/armhf-homeassistant:${tag_r}" \
|
"${docker_reg}/armhf-homeassistant:${tag_r}" \
|
||||||
--os linux --arch arm --variant=v6
|
--os linux --arch arm --variant=v6
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/armv7-homeassistant:${tag_r}" \
|
"${docker_reg}/armv7-homeassistant:${tag_r}" \
|
||||||
--os linux --arch arm --variant=v7
|
--os linux --arch arm --variant=v7
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \
|
||||||
"${registry}/aarch64-homeassistant:${tag_r}" \
|
"${docker_reg}/aarch64-homeassistant:${tag_r}" \
|
||||||
--os linux --arch arm64 --variant=v8
|
--os linux --arch arm64 --variant=v8
|
||||||
|
|
||||||
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
|
docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}"
|
||||||
cosign sign --yes "${registry}/home-assistant:${tag_l}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate_image() {
|
function validate_image() {
|
||||||
local image=${1}
|
local image=${1}
|
||||||
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
|
if ! cas authenticate --signerID notary@home-assistant.io "docker://${image}"; then
|
||||||
echo "Invalid signature!"
|
echo "Invalid signature!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function push_dockerhub() {
|
for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do
|
||||||
local image=${1}
|
docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
local tag=${2}
|
docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
|
validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
docker push "docker.io/homeassistant/${image}:${tag}"
|
validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
|
validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
}
|
validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
# Pull images from github container registry and verify signature
|
|
||||||
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
docker pull "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
docker pull "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
docker pull "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
validate_image "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
validate_image "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
|
|
||||||
# Upload images to dockerhub
|
|
||||||
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create version tag
|
# Create version tag
|
||||||
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
# Create general tags
|
# Create general tags
|
||||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||||
create_manifest "dev" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}"
|
||||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||||
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}"
|
||||||
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}"
|
||||||
else
|
else
|
||||||
create_manifest "stable" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}"
|
||||||
create_manifest "latest" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}"
|
||||||
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}"
|
||||||
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
# Create series version tag (e.g. 2021.6)
|
# Create series version tag (e.g. 2021.6)
|
||||||
v="${{ needs.init.outputs.version }}"
|
v="${{ needs.init.outputs.version }}"
|
||||||
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
|
create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
build_python:
|
|
||||||
name: Build PyPi package
|
|
||||||
environment: ${{ needs.init.outputs.channel }}
|
|
||||||
needs: ["init", "build_base"]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
|
||||||
uses: actions/setup-python@v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
|
|
||||||
- name: Download translations
|
|
||||||
uses: actions/download-artifact@v4.1.8
|
|
||||||
with:
|
|
||||||
name: translations
|
|
||||||
|
|
||||||
- name: Extract translations
|
|
||||||
run: |
|
|
||||||
tar xvf translations.tar.gz
|
|
||||||
rm translations.tar.gz
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
# Remove dist, build, and homeassistant.egg-info
|
|
||||||
# when build locally for testing!
|
|
||||||
pip install twine build
|
|
||||||
python -m build
|
|
||||||
|
|
||||||
- name: Upload package
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
export TWINE_USERNAME="__token__"
|
|
||||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
|
||||||
|
|
||||||
twine upload dist/* --skip-existing
|
|
||||||
|
|
||||||
hassfest-image:
|
|
||||||
name: Build and test hassfest image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
needs: ["init"]
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
env:
|
|
||||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
|
||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
|
||||||
with:
|
|
||||||
context: . # So action will not pull the repository again
|
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
|
||||||
load: true
|
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
|
||||||
|
|
||||||
- name: Run hassfest against core
|
|
||||||
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
|
||||||
id: push
|
|
||||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
|
||||||
with:
|
|
||||||
context: . # So action will not pull the repository again
|
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
|
||||||
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|
1510
.github/workflows/ci.yaml
vendored
34
.github/workflows/codeql.yml
vendored
|
@ -1,34 +0,0 @@
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 18 * * 4"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 360
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3.27.3
|
|
||||||
with:
|
|
||||||
languages: python
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3.27.3
|
|
||||||
with:
|
|
||||||
category: "/language:python"
|
|
2
.github/workflows/lock.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5.0.1
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
|
30
.github/workflows/matchers/flake8.json
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "flake8-error",
|
||||||
|
"severity": "error",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "flake8-warning",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
150
.github/workflows/stale.yml
vendored
|
@ -8,94 +8,84 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# The 60 day stale policy for PRs
|
# The 90 day stale policy
|
||||||
# Used for:
|
# Used for:
|
||||||
# - PRs
|
# - Issues & PRs
|
||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues marked as no-stale or help-wanted
|
||||||
- name: 60 days stale PRs policy
|
- name: 90 days stale issues & PRs policy
|
||||||
uses: actions/stale@v9.0.0
|
uses: actions/stale@v4
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
|
||||||
days-before-close: 7
|
|
||||||
days-before-issue-stale: -1
|
|
||||||
days-before-issue-close: -1
|
|
||||||
operations-per-run: 150
|
|
||||||
remove-stale-when-updated: true
|
|
||||||
stale-pr-label: "stale"
|
|
||||||
exempt-pr-labels: "no-stale"
|
|
||||||
stale-pr-message: >
|
|
||||||
There hasn't been any activity on this pull request recently. This
|
|
||||||
pull request has been automatically marked as stale because of that
|
|
||||||
and will be closed if no further activity occurs within 7 days.
|
|
||||||
|
|
||||||
If you are the author of this PR, please leave a comment if you want
|
|
||||||
to keep it open. Also, please rebase your PR onto the latest dev
|
|
||||||
branch to ensure that it's up to date with the latest changes.
|
|
||||||
|
|
||||||
Thank you for your contribution!
|
|
||||||
|
|
||||||
# Generate a token for the GitHub App, we use this method to avoid
|
|
||||||
# hitting API limits for our GitHub actions + have a higher rate limit.
|
|
||||||
# This is only used for issues.
|
|
||||||
- name: Generate app token
|
|
||||||
id: token
|
|
||||||
# Pinned to a specific version of the action for security reasons
|
|
||||||
# v1.7.0
|
|
||||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
|
||||||
with:
|
|
||||||
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
|
|
||||||
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
|
|
||||||
|
|
||||||
# The 90 day stale policy for issues
|
|
||||||
# Used for:
|
|
||||||
# - Issues
|
|
||||||
# - No issues marked as no-stale or help-wanted
|
|
||||||
# - No PRs (-1)
|
|
||||||
- name: 90 days stale issues
|
|
||||||
uses: actions/stale@v9.0.0
|
|
||||||
with:
|
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
days-before-close: 7
|
days-before-close: 7
|
||||||
days-before-pr-stale: -1
|
operations-per-run: 150
|
||||||
days-before-pr-close: -1
|
remove-stale-when-updated: true
|
||||||
operations-per-run: 250
|
stale-issue-label: "stale"
|
||||||
remove-stale-when-updated: true
|
exempt-issue-labels: "no-stale,help-wanted"
|
||||||
stale-issue-label: "stale"
|
stale-issue-message: >
|
||||||
exempt-issue-labels: "no-stale,help-wanted,needs-more-information"
|
There hasn't been any activity on this issue recently. Due to the
|
||||||
stale-issue-message: >
|
high number of incoming GitHub notifications, we have to clean some
|
||||||
There hasn't been any activity on this issue recently. Due to the
|
of the old issues, as many of them have already been resolved with
|
||||||
high number of incoming GitHub notifications, we have to clean some
|
the latest updates.
|
||||||
of the old issues, as many of them have already been resolved with
|
|
||||||
the latest updates.
|
Please make sure to update to the latest Home Assistant version and
|
||||||
|
check if that solves the issue. Let us know if that works for you by
|
||||||
Please make sure to update to the latest Home Assistant version and
|
adding a comment 👍
|
||||||
check if that solves the issue. Let us know if that works for you by
|
|
||||||
adding a comment 👍
|
This issue has now been marked as stale and will be closed if no
|
||||||
|
further activity occurs. Thank you for your contributions.
|
||||||
This issue has now been marked as stale and will be closed if no
|
|
||||||
further activity occurs. Thank you for your contributions.
|
stale-pr-label: "stale"
|
||||||
|
exempt-pr-labels: "no-stale"
|
||||||
# The 30 day stale policy for issues
|
stale-pr-message: >
|
||||||
# Used for:
|
There hasn't been any activity on this pull request recently. This
|
||||||
# - Issues that are pending more information (incomplete issues)
|
pull request has been automatically marked as stale because of that
|
||||||
# - No Issues marked as no-stale or help-wanted
|
and will be closed if no further activity occurs within 7 days.
|
||||||
# - No PRs (-1)
|
|
||||||
- name: Needs more information stale issues policy
|
Thank you for your contributions.
|
||||||
uses: actions/stale@v9.0.0
|
|
||||||
with:
|
# The 30 day stale policy for PRS
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
# Used for:
|
||||||
only-labels: "needs-more-information"
|
# - PRs
|
||||||
days-before-stale: 14
|
# - No PRs marked as no-stale or new-integrations
|
||||||
days-before-close: 7
|
# - No issues (-1)
|
||||||
days-before-pr-stale: -1
|
- name: 30 days stale PRs policy
|
||||||
days-before-pr-close: -1
|
uses: actions/stale@v4
|
||||||
operations-per-run: 250
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 7
|
||||||
|
days-before-issue-close: -1
|
||||||
|
operations-per-run: 50
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
# Exempt new integrations, these often take more time.
|
||||||
|
# They will automatically be handled by the 90 day version above.
|
||||||
|
exempt-pr-labels: "no-stale,new-integration"
|
||||||
|
stale-pr-message: >
|
||||||
|
There hasn't been any activity on this pull request recently. This
|
||||||
|
pull request has been automatically marked as stale because of that
|
||||||
|
and will be closed if no further activity occurs within 7 days.
|
||||||
|
|
||||||
|
Thank you for your contributions.
|
||||||
|
|
||||||
|
# The 30 day stale policy for issues
|
||||||
|
# Used for:
|
||||||
|
# - Issues that are pending more information (incomplete issues)
|
||||||
|
# - No Issues marked as no-stale or help-wanted
|
||||||
|
# - No PRs (-1)
|
||||||
|
- name: Needs more information stale issues policy
|
||||||
|
uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
only-labels: "needs-more-information"
|
||||||
|
days-before-stale: 14
|
||||||
|
days-before-close: 7
|
||||||
|
days-before-pr-close: -1
|
||||||
|
operations-per-run: 50
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
exempt-issue-labels: "no-stale,help-wanted"
|
exempt-issue-labels: "no-stale,help-wanted"
|
||||||
|
|
65
.github/workflows/translations.yaml
vendored
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
name: Translations
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- "**strings.json"
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: 3.9
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload:
|
||||||
|
name: Upload
|
||||||
|
if: github.repository_owner == 'home-assistant'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.3.2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
- name: Upload Translations
|
||||||
|
run: |
|
||||||
|
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||||
|
python3 -m script.translations upload
|
||||||
|
|
||||||
|
download:
|
||||||
|
name: Download
|
||||||
|
needs: upload
|
||||||
|
if: github.repository_owner == 'home-assistant' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.3.2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
- name: Download Translations
|
||||||
|
run: |
|
||||||
|
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||||
|
python3 -m script.translations download
|
||||||
|
|
||||||
|
- name: Initialize git
|
||||||
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
|
with:
|
||||||
|
name: GitHub Action
|
||||||
|
email: github-action@users.noreply.github.com
|
||||||
|
|
||||||
|
- name: Update translation
|
||||||
|
run: |
|
||||||
|
git add homeassistant
|
||||||
|
git commit -am "[ci skip] Translation update"
|
||||||
|
git push
|
32
.github/workflows/translations.yml
vendored
|
@ -1,32 +0,0 @@
|
||||||
name: Translations
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
paths:
|
|
||||||
- "**strings.json"
|
|
||||||
|
|
||||||
env:
|
|
||||||
DEFAULT_PYTHON: "3.12"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upload:
|
|
||||||
name: Upload
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
|
||||||
uses: actions/setup-python@v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
|
|
||||||
- name: Upload Translations
|
|
||||||
run: |
|
|
||||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
|
||||||
python3 -m script.translations upload
|
|
228
.github/workflows/wheels.yml
vendored
|
@ -10,18 +10,8 @@ on:
|
||||||
- dev
|
- dev
|
||||||
- rc
|
- rc
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/wheels.yml"
|
|
||||||
- "homeassistant/package_constraints.txt"
|
|
||||||
- "requirements_all.txt"
|
|
||||||
- "requirements.txt"
|
- "requirements.txt"
|
||||||
- "script/gen_requirements_all.py"
|
- "requirements_all.txt"
|
||||||
|
|
||||||
env:
|
|
||||||
DEFAULT_PYTHON: "3.12"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
|
@ -32,22 +22,7 @@ jobs:
|
||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
|
||||||
id: python
|
|
||||||
uses: actions/setup-python@v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
check-latest: true
|
|
||||||
|
|
||||||
- name: Create Python virtual environment
|
|
||||||
run: |
|
|
||||||
python -m venv venv
|
|
||||||
. venv/bin/activate
|
|
||||||
python --version
|
|
||||||
pip install "$(grep '^uv' < requirements.txt)"
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
|
|
||||||
- name: Get information
|
- name: Get information
|
||||||
id: info
|
id: info
|
||||||
|
@ -64,204 +39,135 @@ jobs:
|
||||||
- name: Write env-file
|
- name: Write env-file
|
||||||
run: |
|
run: |
|
||||||
(
|
(
|
||||||
|
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
|
||||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
||||||
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
||||||
|
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
|
||||||
# Fix out of memory issues with rust
|
# GRPC on armv7 needs -lexecinfo (issue #56669) since home assistant installs
|
||||||
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
# execinfo-dev when building wheels. The setup.py does not have an option for
|
||||||
|
# adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0)
|
||||||
# OpenCV headless installation
|
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo"
|
||||||
echo "CI_BUILD=1"
|
|
||||||
echo "ENABLE_HEADLESS=1"
|
|
||||||
|
|
||||||
# Use C-Extension for SQLAlchemy
|
|
||||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
|
||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v2.3.1
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
include-hidden-files: true
|
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v2.3.1
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Generate requirements
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
python -m script.gen_requirements_all ci
|
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
|
||||||
uses: actions/upload-artifact@v4.4.3
|
|
||||||
with:
|
|
||||||
name: requirements_all_wheels
|
|
||||||
path: ./requirements_all_wheels_*.txt
|
|
||||||
|
|
||||||
core:
|
core:
|
||||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for core
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
abi: ["cp312", "cp313"]
|
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
|
tag:
|
||||||
|
- "3.9-alpine3.14"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Adjust build env
|
|
||||||
run: |
|
|
||||||
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
|
||||||
sed -i "/uv/d" requirements.txt
|
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2024.11.0
|
uses: home-assistant/wheels@2022.01.2
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
tag: ${{ matrix.tag }}
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
wheels-host: wheels.hass.io
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
wheels-user: wheels
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
|
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo"
|
||||||
skip-binary: aiohttp;multidict;yarl
|
pip: "Cython;numpy"
|
||||||
|
skip-binary: aiohttp
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: 'requirements_diff.txt'
|
||||||
requirements: "requirements.txt"
|
requirements: "requirements.txt"
|
||||||
|
|
||||||
integrations:
|
integrations:
|
||||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
abi: ["cp312", "cp313"]
|
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
|
tag:
|
||||||
|
- "3.9-alpine3.14"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v2.4.0
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Uncomment packages
|
||||||
uses: actions/download-artifact@v4.1.8
|
|
||||||
with:
|
|
||||||
name: requirements_all_wheels
|
|
||||||
|
|
||||||
- name: Adjust build env
|
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.arch }}" = "i386" ]; then
|
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||||
echo "NPY_DISABLE_SVML=1" >> .env_file
|
for requirement_file in ${requirement_files}; do
|
||||||
fi
|
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||||
|
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
|
||||||
|
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||||
|
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
|
||||||
|
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
|
||||||
|
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
|
||||||
|
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
|
||||||
|
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||||
|
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||||
|
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||||
|
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
|
||||||
|
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
|
||||||
|
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
|
||||||
|
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||||
|
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||||
|
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||||
|
sed -i "s|# decora|decora|g" ${requirement_file}
|
||||||
|
sed -i "s|# avion|avion|g" ${requirement_file}
|
||||||
|
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
|
||||||
|
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
|
||||||
|
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
|
||||||
|
sed -i "s|# bme680|bme680|g" ${requirement_file}
|
||||||
|
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||||
|
done
|
||||||
|
|
||||||
# Do not pin numpy in wheels building
|
- name: Build wheels
|
||||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
uses: home-assistant/wheels@2022.01.2
|
||||||
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
|
||||||
sed -i "/uv/d" requirements.txt
|
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
|
||||||
|
|
||||||
- name: Split requirements all
|
|
||||||
run: |
|
|
||||||
# We split requirements all into multiple files.
|
|
||||||
# This is to prevent the build from running out of memory when
|
|
||||||
# resolving packages on 32-bits systems (like armhf, armv7).
|
|
||||||
|
|
||||||
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
|
|
||||||
|
|
||||||
- name: Create requirements for cython<3
|
|
||||||
if: matrix.abi == 'cp312'
|
|
||||||
run: |
|
|
||||||
# Some dependencies still require 'cython<3'
|
|
||||||
# and don't yet use isolated build environments.
|
|
||||||
# Build these first.
|
|
||||||
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
|
||||||
|
|
||||||
touch requirements_old-cython.txt
|
|
||||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
|
||||||
uses: home-assistant/wheels@2024.11.0
|
|
||||||
if: matrix.abi == 'cp312'
|
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
tag: ${{ matrix.tag }}
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
wheels-host: wheels.hass.io
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
wheels-user: wheels
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;cargo"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
pip: "Cython;numpy;scikit-build"
|
||||||
|
skip-binary: aiohttp,grpcio
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: 'requirements_diff.txt'
|
||||||
requirements: "requirements_old-cython.txt"
|
requirements: "requirements_all.txt"
|
||||||
pip: "'cython<3'"
|
|
||||||
|
|
||||||
- name: Build wheels (part 1)
|
|
||||||
uses: home-assistant/wheels@2024.11.0
|
|
||||||
with:
|
|
||||||
abi: ${{ matrix.abi }}
|
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
|
||||||
env-file: true
|
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
|
||||||
requirements-diff: "requirements_diff.txt"
|
|
||||||
requirements: "requirements_all.txtaa"
|
|
||||||
|
|
||||||
- name: Build wheels (part 2)
|
|
||||||
uses: home-assistant/wheels@2024.11.0
|
|
||||||
with:
|
|
||||||
abi: ${{ matrix.abi }}
|
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
|
||||||
env-file: true
|
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
|
||||||
requirements-diff: "requirements_diff.txt"
|
|
||||||
requirements: "requirements_all.txtab"
|
|
||||||
|
|
||||||
- name: Build wheels (part 3)
|
|
||||||
uses: home-assistant/wheels@2024.11.0
|
|
||||||
with:
|
|
||||||
abi: ${{ matrix.abi }}
|
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
|
||||||
env-file: true
|
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
|
||||||
requirements-diff: "requirements_diff.txt"
|
|
||||||
requirements: "requirements_all.txtac"
|
|
||||||
|
|
13
.gitignore
vendored
|
@ -8,9 +8,6 @@ tests/testing_config/home-assistant.log*
|
||||||
data/
|
data/
|
||||||
.token
|
.token
|
||||||
|
|
||||||
# Translations
|
|
||||||
homeassistant/components/*/translations
|
|
||||||
|
|
||||||
# Hide sublime text stuff
|
# Hide sublime text stuff
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
@ -34,7 +31,6 @@ Icon
|
||||||
|
|
||||||
# GITHUB Proposed Python stuff:
|
# GITHUB Proposed Python stuff:
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
__pycache__
|
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
@ -62,13 +58,13 @@ pip-log.txt
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
.coverage
|
.coverage
|
||||||
|
.tox
|
||||||
coverage.xml
|
coverage.xml
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
htmlcov/
|
htmlcov/
|
||||||
test-reports/
|
test-reports/
|
||||||
test-results.xml
|
test-results.xml
|
||||||
test-output.xml
|
test-output.xml
|
||||||
pytest-*.txt
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
@ -79,7 +75,6 @@ pytest-*.txt
|
||||||
.pydevproject
|
.pydevproject
|
||||||
|
|
||||||
.python-version
|
.python-version
|
||||||
.tool-versions
|
|
||||||
|
|
||||||
# emacs auto backups
|
# emacs auto backups
|
||||||
*~
|
*~
|
||||||
|
@ -113,6 +108,9 @@ virtualization/vagrant/config
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Built docs
|
||||||
|
docs/build
|
||||||
|
|
||||||
# Windows Explorer
|
# Windows Explorer
|
||||||
desktop.ini
|
desktop.ini
|
||||||
/home-assistant.pyproj
|
/home-assistant.pyproj
|
||||||
|
@ -134,6 +132,3 @@ tmp_cache
|
||||||
|
|
||||||
# python-language-server / Rope
|
# python-language-server / Rope
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
# Will be created from script/split_tests.py
|
|
||||||
pytest_buckets.txt
|
|
|
@ -3,4 +3,3 @@ ignored:
|
||||||
- DL3008
|
- DL3008
|
||||||
- DL3013
|
- DL3013
|
||||||
- DL3018
|
- DL3018
|
||||||
- DL3042
|
|
||||||
|
|
6
.ignore
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Patterns matched in this file will be ignored by supported search utilities
|
||||||
|
|
||||||
|
# Ignore generated html and javascript files
|
||||||
|
/homeassistant/components/frontend/www_static/*.html
|
||||||
|
/homeassistant/components/frontend/www_static/*.js
|
||||||
|
/homeassistant/components/frontend/www_static/panels/*.html
|
|
@ -1,24 +1,55 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v0.7.3
|
rev: v2.31.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: pyupgrade
|
||||||
|
args: [--py39-plus]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.1.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
args:
|
args:
|
||||||
- --fix
|
- --safe
|
||||||
- id: ruff-format
|
- --quiet
|
||||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.3.0
|
rev: v2.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json]
|
||||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
exclude: ^tests/fixtures/
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 4.0.1
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- pycodestyle==2.8.0
|
||||||
|
- pyflakes==2.4.0
|
||||||
|
- flake8-docstrings==1.6.0
|
||||||
|
- pydocstyle==6.1.1
|
||||||
|
- flake8-comprehensions==3.7.0
|
||||||
|
- flake8-noqa==1.2.1
|
||||||
|
- mccabe==0.6.1
|
||||||
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
rev: 1.7.0
|
||||||
|
hooks:
|
||||||
|
- id: bandit
|
||||||
|
args:
|
||||||
|
- --quiet
|
||||||
|
- --format=custom
|
||||||
|
- --configfile=tests/bandit.yaml
|
||||||
|
files: ^(homeassistant|script|tests)/.+\.py$
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.10.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v3.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
|
@ -30,24 +61,24 @@ repos:
|
||||||
- --branch=master
|
- --branch=master
|
||||||
- --branch=rc
|
- --branch=rc
|
||||||
- repo: https://github.com/adrienverge/yamllint.git
|
- repo: https://github.com/adrienverge/yamllint.git
|
||||||
rev: v1.35.1
|
rev: v1.26.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamllint
|
- id: yamllint
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v3.0.3
|
rev: v2.2.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
|
stages: [manual]
|
||||||
- repo: https://github.com/cdce8p/python-typing-update
|
- repo: https://github.com/cdce8p/python-typing-update
|
||||||
rev: v0.6.0
|
rev: v0.3.5
|
||||||
hooks:
|
hooks:
|
||||||
# Run `python-typing-update` hook manually from time to time
|
# Run `python-typing-update` hook manually from time to time
|
||||||
# to update python typing syntax.
|
# to update python typing syntax.
|
||||||
# Will require manual work, before submitting changes!
|
# Will require manual work, before submitting changes!
|
||||||
# pre-commit run --hook-stage manual python-typing-update --all-files
|
|
||||||
- id: python-typing-update
|
- id: python-typing-update
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
args:
|
args:
|
||||||
- --py311-plus
|
- --py39-plus
|
||||||
- --force
|
- --force
|
||||||
- --keep-updates
|
- --keep-updates
|
||||||
files: ^(homeassistant|tests|script)/.+\.py$
|
files: ^(homeassistant|tests|script)/.+\.py$
|
||||||
|
@ -61,36 +92,36 @@ repos:
|
||||||
name: mypy
|
name: mypy
|
||||||
entry: script/run-in-env.sh mypy
|
entry: script/run-in-env.sh mypy
|
||||||
language: script
|
language: script
|
||||||
types_or: [python, pyi]
|
types: [python]
|
||||||
require_serial: true
|
require_serial: true
|
||||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
files: ^homeassistant/.+\.py$
|
||||||
- id: pylint
|
- id: pylint
|
||||||
name: pylint
|
name: pylint
|
||||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
entry: script/run-in-env.sh pylint -j 0
|
||||||
language: script
|
language: script
|
||||||
types_or: [python, pyi]
|
types: [python]
|
||||||
files: ^(homeassistant|tests)/.+\.(py|pyi)$
|
files: ^homeassistant/.+\.py$
|
||||||
- id: gen_requirements_all
|
- id: gen_requirements_all
|
||||||
name: gen_requirements_all
|
name: gen_requirements_all
|
||||||
entry: script/run-in-env.sh python3 -m script.gen_requirements_all
|
entry: script/run-in-env.sh python3 -m script.gen_requirements_all
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
||||||
- id: hassfest
|
- id: hassfest
|
||||||
name: hassfest
|
name: hassfest
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest
|
entry: script/run-in-env.sh python3 -m script.hassfest
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$
|
||||||
- id: hassfest-metadata
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
|
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|setup\.cfg)$
|
||||||
- id: hassfest-mypy-config
|
- id: hassfest-mypy-config
|
||||||
name: hassfest-mypy-config
|
name: hassfest-mypy-config
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
*.md
|
*.md
|
||||||
.strict-typing
|
azure-*.yml
|
||||||
|
docs/source/_templates/*
|
||||||
homeassistant/components/*/translations/*.json
|
homeassistant/components/*/translations/*.json
|
||||||
homeassistant/generated/*
|
tests/fixtures/*
|
||||||
tests/components/lidarr/fixtures/initialize.js
|
|
||||||
tests/components/lidarr/fixtures/initialize-wrong.js
|
|
||||||
tests/fixtures/core/config/yaml_errors/
|
|
||||||
|
|
14
.readthedocs.yml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# .readthedocs.yml
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.9"
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: setuptools
|
||||||
|
path: .
|
||||||
|
- requirements: requirements_docs.txt
|
366
.strict-typing
|
@ -2,524 +2,214 @@
|
||||||
# If component is fully covered with type annotations, please add it here
|
# If component is fully covered with type annotations, please add it here
|
||||||
# to enable strict mypy checks.
|
# to enable strict mypy checks.
|
||||||
|
|
||||||
# Strict typing is enabled by default for core files.
|
# Stict typing is enabled by default for core files.
|
||||||
# Add it here to add 'disallow_any_generics'.
|
# Add it here to add 'disallow_any_generics'.
|
||||||
# --- Only for core file! ---
|
# --- Only for core file! ---
|
||||||
homeassistant.auth.auth_store
|
|
||||||
homeassistant.auth.providers.*
|
|
||||||
homeassistant.core
|
|
||||||
homeassistant.exceptions
|
homeassistant.exceptions
|
||||||
homeassistant.helpers.area_registry
|
homeassistant.core
|
||||||
homeassistant.helpers.condition
|
|
||||||
homeassistant.helpers.debounce
|
|
||||||
homeassistant.helpers.deprecation
|
|
||||||
homeassistant.helpers.device_registry
|
|
||||||
homeassistant.helpers.discovery
|
|
||||||
homeassistant.helpers.dispatcher
|
|
||||||
homeassistant.helpers.entity
|
|
||||||
homeassistant.helpers.entity_platform
|
|
||||||
homeassistant.helpers.entity_values
|
|
||||||
homeassistant.helpers.event
|
|
||||||
homeassistant.helpers.reload
|
|
||||||
homeassistant.helpers.script
|
|
||||||
homeassistant.helpers.script_variables
|
|
||||||
homeassistant.helpers.singleton
|
|
||||||
homeassistant.helpers.sun
|
|
||||||
homeassistant.helpers.translation
|
|
||||||
homeassistant.loader
|
homeassistant.loader
|
||||||
homeassistant.requirements
|
homeassistant.requirements
|
||||||
homeassistant.runner
|
homeassistant.runner
|
||||||
homeassistant.setup
|
homeassistant.setup
|
||||||
|
homeassistant.auth.auth_store
|
||||||
|
homeassistant.auth.providers.*
|
||||||
|
homeassistant.helpers.area_registry
|
||||||
|
homeassistant.helpers.condition
|
||||||
|
homeassistant.helpers.discovery
|
||||||
|
homeassistant.helpers.entity_values
|
||||||
|
homeassistant.helpers.reload
|
||||||
|
homeassistant.helpers.script_variables
|
||||||
|
homeassistant.helpers.translation
|
||||||
homeassistant.util.async_
|
homeassistant.util.async_
|
||||||
homeassistant.util.color
|
homeassistant.util.color
|
||||||
homeassistant.util.decorator
|
|
||||||
homeassistant.util.location
|
|
||||||
homeassistant.util.logging
|
|
||||||
homeassistant.util.process
|
homeassistant.util.process
|
||||||
homeassistant.util.unit_system
|
homeassistant.util.unit_system
|
||||||
|
|
||||||
# --- Add components below this line ---
|
# --- Add components below this line ---
|
||||||
homeassistant.components
|
homeassistant.components
|
||||||
homeassistant.components.abode.*
|
homeassistant.components.abode.*
|
||||||
homeassistant.components.accuweather.*
|
|
||||||
homeassistant.components.acer_projector.*
|
homeassistant.components.acer_projector.*
|
||||||
homeassistant.components.acmeda.*
|
homeassistant.components.accuweather.*
|
||||||
homeassistant.components.actiontec.*
|
homeassistant.components.actiontec.*
|
||||||
homeassistant.components.adax.*
|
|
||||||
homeassistant.components.adguard.*
|
|
||||||
homeassistant.components.aftership.*
|
homeassistant.components.aftership.*
|
||||||
homeassistant.components.air_quality.*
|
homeassistant.components.air_quality.*
|
||||||
homeassistant.components.airgradient.*
|
|
||||||
homeassistant.components.airly.*
|
homeassistant.components.airly.*
|
||||||
homeassistant.components.airnow.*
|
|
||||||
homeassistant.components.airq.*
|
|
||||||
homeassistant.components.airthings.*
|
|
||||||
homeassistant.components.airthings_ble.*
|
|
||||||
homeassistant.components.airtouch5.*
|
|
||||||
homeassistant.components.airvisual.*
|
homeassistant.components.airvisual.*
|
||||||
homeassistant.components.airvisual_pro.*
|
|
||||||
homeassistant.components.airzone.*
|
|
||||||
homeassistant.components.airzone_cloud.*
|
|
||||||
homeassistant.components.aladdin_connect.*
|
homeassistant.components.aladdin_connect.*
|
||||||
homeassistant.components.alarm_control_panel.*
|
homeassistant.components.alarm_control_panel.*
|
||||||
homeassistant.components.alert.*
|
|
||||||
homeassistant.components.alexa.*
|
|
||||||
homeassistant.components.alpha_vantage.*
|
|
||||||
homeassistant.components.amazon_polly.*
|
homeassistant.components.amazon_polly.*
|
||||||
homeassistant.components.amberelectric.*
|
homeassistant.components.ambee.*
|
||||||
homeassistant.components.ambient_network.*
|
|
||||||
homeassistant.components.ambient_station.*
|
homeassistant.components.ambient_station.*
|
||||||
homeassistant.components.amcrest.*
|
homeassistant.components.amcrest.*
|
||||||
homeassistant.components.ampio.*
|
homeassistant.components.ampio.*
|
||||||
homeassistant.components.analytics.*
|
|
||||||
homeassistant.components.analytics_insights.*
|
|
||||||
homeassistant.components.android_ip_webcam.*
|
|
||||||
homeassistant.components.androidtv.*
|
|
||||||
homeassistant.components.androidtv_remote.*
|
|
||||||
homeassistant.components.anel_pwrctrl.*
|
|
||||||
homeassistant.components.anova.*
|
|
||||||
homeassistant.components.anthemav.*
|
|
||||||
homeassistant.components.apache_kafka.*
|
|
||||||
homeassistant.components.apcupsd.*
|
|
||||||
homeassistant.components.api.*
|
|
||||||
homeassistant.components.apple_tv.*
|
|
||||||
homeassistant.components.apprise.*
|
|
||||||
homeassistant.components.aprs.*
|
|
||||||
homeassistant.components.apsystems.*
|
|
||||||
homeassistant.components.aqualogic.*
|
|
||||||
homeassistant.components.aquostv.*
|
|
||||||
homeassistant.components.aranet.*
|
|
||||||
homeassistant.components.arcam_fmj.*
|
|
||||||
homeassistant.components.arris_tg2492lg.*
|
|
||||||
homeassistant.components.aruba.*
|
|
||||||
homeassistant.components.arwn.*
|
|
||||||
homeassistant.components.aseko_pool_live.*
|
homeassistant.components.aseko_pool_live.*
|
||||||
homeassistant.components.assist_pipeline.*
|
|
||||||
homeassistant.components.assist_satellite.*
|
|
||||||
homeassistant.components.asuswrt.*
|
|
||||||
homeassistant.components.autarco.*
|
|
||||||
homeassistant.components.auth.*
|
|
||||||
homeassistant.components.automation.*
|
homeassistant.components.automation.*
|
||||||
homeassistant.components.awair.*
|
|
||||||
homeassistant.components.axis.*
|
|
||||||
homeassistant.components.backup.*
|
|
||||||
homeassistant.components.baf.*
|
|
||||||
homeassistant.components.bang_olufsen.*
|
|
||||||
homeassistant.components.bayesian.*
|
|
||||||
homeassistant.components.binary_sensor.*
|
homeassistant.components.binary_sensor.*
|
||||||
homeassistant.components.bitcoin.*
|
|
||||||
homeassistant.components.blockchain.*
|
|
||||||
homeassistant.components.blue_current.*
|
|
||||||
homeassistant.components.blueprint.*
|
|
||||||
homeassistant.components.bluesound.*
|
|
||||||
homeassistant.components.bluetooth.*
|
|
||||||
homeassistant.components.bluetooth_adapters.*
|
|
||||||
homeassistant.components.bluetooth_tracker.*
|
homeassistant.components.bluetooth_tracker.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
homeassistant.components.brother.*
|
homeassistant.components.brother.*
|
||||||
homeassistant.components.browser.*
|
homeassistant.components.browser.*
|
||||||
homeassistant.components.bryant_evolution.*
|
|
||||||
homeassistant.components.bthome.*
|
|
||||||
homeassistant.components.button.*
|
homeassistant.components.button.*
|
||||||
homeassistant.components.calendar.*
|
homeassistant.components.calendar.*
|
||||||
homeassistant.components.cambridge_audio.*
|
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
homeassistant.components.cert_expiry.*
|
|
||||||
homeassistant.components.clickatell.*
|
|
||||||
homeassistant.components.clicksend.*
|
|
||||||
homeassistant.components.climate.*
|
|
||||||
homeassistant.components.cloud.*
|
|
||||||
homeassistant.components.co2signal.*
|
|
||||||
homeassistant.components.command_line.*
|
|
||||||
homeassistant.components.config.*
|
|
||||||
homeassistant.components.configurator.*
|
|
||||||
homeassistant.components.counter.*
|
|
||||||
homeassistant.components.cover.*
|
homeassistant.components.cover.*
|
||||||
homeassistant.components.cpuspeed.*
|
|
||||||
homeassistant.components.crownstone.*
|
homeassistant.components.crownstone.*
|
||||||
homeassistant.components.date.*
|
homeassistant.components.cpuspeed.*
|
||||||
homeassistant.components.datetime.*
|
|
||||||
homeassistant.components.deako.*
|
|
||||||
homeassistant.components.deconz.*
|
|
||||||
homeassistant.components.default_config.*
|
|
||||||
homeassistant.components.demo.*
|
|
||||||
homeassistant.components.derivative.*
|
|
||||||
homeassistant.components.device_automation.*
|
homeassistant.components.device_automation.*
|
||||||
homeassistant.components.device_tracker.*
|
homeassistant.components.device_tracker.*
|
||||||
homeassistant.components.devolo_home_control.*
|
homeassistant.components.devolo_home_control.*
|
||||||
homeassistant.components.devolo_home_network.*
|
homeassistant.components.devolo_home_network.*
|
||||||
homeassistant.components.dhcp.*
|
|
||||||
homeassistant.components.diagnostics.*
|
|
||||||
homeassistant.components.discovergy.*
|
|
||||||
homeassistant.components.dlna_dmr.*
|
homeassistant.components.dlna_dmr.*
|
||||||
homeassistant.components.dlna_dms.*
|
|
||||||
homeassistant.components.dnsip.*
|
homeassistant.components.dnsip.*
|
||||||
homeassistant.components.doorbird.*
|
|
||||||
homeassistant.components.dormakaba_dkey.*
|
|
||||||
homeassistant.components.downloader.*
|
|
||||||
homeassistant.components.dsmr.*
|
homeassistant.components.dsmr.*
|
||||||
homeassistant.components.duckdns.*
|
|
||||||
homeassistant.components.dunehd.*
|
homeassistant.components.dunehd.*
|
||||||
homeassistant.components.duotecno.*
|
|
||||||
homeassistant.components.easyenergy.*
|
|
||||||
homeassistant.components.ecovacs.*
|
|
||||||
homeassistant.components.ecowitt.*
|
|
||||||
homeassistant.components.efergy.*
|
homeassistant.components.efergy.*
|
||||||
homeassistant.components.electrasmart.*
|
|
||||||
homeassistant.components.electric_kiwi.*
|
|
||||||
homeassistant.components.elevenlabs.*
|
|
||||||
homeassistant.components.elgato.*
|
homeassistant.components.elgato.*
|
||||||
homeassistant.components.elkm1.*
|
|
||||||
homeassistant.components.emulated_hue.*
|
|
||||||
homeassistant.components.energenie_power_sockets.*
|
|
||||||
homeassistant.components.energy.*
|
|
||||||
homeassistant.components.energyzero.*
|
|
||||||
homeassistant.components.enigma2.*
|
|
||||||
homeassistant.components.enphase_envoy.*
|
|
||||||
homeassistant.components.eq3btsmart.*
|
|
||||||
homeassistant.components.esphome.*
|
homeassistant.components.esphome.*
|
||||||
homeassistant.components.event.*
|
homeassistant.components.energy.*
|
||||||
homeassistant.components.evil_genius_labs.*
|
homeassistant.components.evil_genius_labs.*
|
||||||
homeassistant.components.evohome.*
|
|
||||||
homeassistant.components.faa_delays.*
|
|
||||||
homeassistant.components.fan.*
|
|
||||||
homeassistant.components.fastdotcom.*
|
homeassistant.components.fastdotcom.*
|
||||||
homeassistant.components.feedreader.*
|
|
||||||
homeassistant.components.file_upload.*
|
|
||||||
homeassistant.components.filesize.*
|
|
||||||
homeassistant.components.filter.*
|
|
||||||
homeassistant.components.fitbit.*
|
homeassistant.components.fitbit.*
|
||||||
homeassistant.components.flexit_bacnet.*
|
homeassistant.components.flunearyou.*
|
||||||
homeassistant.components.flux_led.*
|
homeassistant.components.flux_led.*
|
||||||
homeassistant.components.forecast_solar.*
|
homeassistant.components.forecast_solar.*
|
||||||
homeassistant.components.fritz.*
|
|
||||||
homeassistant.components.fritzbox.*
|
homeassistant.components.fritzbox.*
|
||||||
homeassistant.components.fritzbox_callmonitor.*
|
|
||||||
homeassistant.components.fronius.*
|
homeassistant.components.fronius.*
|
||||||
homeassistant.components.frontend.*
|
homeassistant.components.frontend.*
|
||||||
homeassistant.components.fujitsu_fglair.*
|
homeassistant.components.fritz.*
|
||||||
homeassistant.components.fully_kiosk.*
|
|
||||||
homeassistant.components.fyta.*
|
|
||||||
homeassistant.components.generic_hygrostat.*
|
|
||||||
homeassistant.components.generic_thermostat.*
|
|
||||||
homeassistant.components.geo_location.*
|
homeassistant.components.geo_location.*
|
||||||
homeassistant.components.geocaching.*
|
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
homeassistant.components.glances.*
|
|
||||||
homeassistant.components.go2rtc.*
|
|
||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
homeassistant.components.google.*
|
|
||||||
homeassistant.components.google_assistant_sdk.*
|
|
||||||
homeassistant.components.google_cloud.*
|
|
||||||
homeassistant.components.google_photos.*
|
|
||||||
homeassistant.components.google_sheets.*
|
|
||||||
homeassistant.components.govee_ble.*
|
|
||||||
homeassistant.components.gpsd.*
|
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
homeassistant.components.group.*
|
homeassistant.components.group.*
|
||||||
homeassistant.components.guardian.*
|
homeassistant.components.guardian.*
|
||||||
homeassistant.components.hardkernel.*
|
|
||||||
homeassistant.components.hardware.*
|
|
||||||
homeassistant.components.here_travel_time.*
|
|
||||||
homeassistant.components.history.*
|
homeassistant.components.history.*
|
||||||
homeassistant.components.history_stats.*
|
homeassistant.components.homeassistant.triggers.event
|
||||||
homeassistant.components.holiday.*
|
|
||||||
homeassistant.components.homeassistant.*
|
|
||||||
homeassistant.components.homeassistant_alerts.*
|
|
||||||
homeassistant.components.homeassistant_green.*
|
|
||||||
homeassistant.components.homeassistant_hardware.*
|
|
||||||
homeassistant.components.homeassistant_sky_connect.*
|
|
||||||
homeassistant.components.homeassistant_yellow.*
|
|
||||||
homeassistant.components.homekit.*
|
|
||||||
homeassistant.components.homekit_controller
|
homeassistant.components.homekit_controller
|
||||||
homeassistant.components.homekit_controller.alarm_control_panel
|
homeassistant.components.homekit_controller.alarm_control_panel
|
||||||
homeassistant.components.homekit_controller.button
|
homeassistant.components.homekit_controller.button
|
||||||
homeassistant.components.homekit_controller.config_flow
|
|
||||||
homeassistant.components.homekit_controller.const
|
homeassistant.components.homekit_controller.const
|
||||||
homeassistant.components.homekit_controller.lock
|
homeassistant.components.homekit_controller.lock
|
||||||
homeassistant.components.homekit_controller.select
|
homeassistant.components.homekit_controller.select
|
||||||
homeassistant.components.homekit_controller.storage
|
homeassistant.components.homekit_controller.storage
|
||||||
homeassistant.components.homekit_controller.utils
|
homeassistant.components.homekit_controller.utils
|
||||||
homeassistant.components.homewizard.*
|
homeassistant.components.homewizard.*
|
||||||
homeassistant.components.homeworks.*
|
|
||||||
homeassistant.components.http.*
|
homeassistant.components.http.*
|
||||||
homeassistant.components.huawei_lte.*
|
homeassistant.components.huawei_lte.*
|
||||||
homeassistant.components.humidifier.*
|
|
||||||
homeassistant.components.husqvarna_automower.*
|
|
||||||
homeassistant.components.hydrawise.*
|
|
||||||
homeassistant.components.hyperion.*
|
homeassistant.components.hyperion.*
|
||||||
homeassistant.components.ibeacon.*
|
|
||||||
homeassistant.components.idasen_desk.*
|
|
||||||
homeassistant.components.image.*
|
|
||||||
homeassistant.components.image_processing.*
|
homeassistant.components.image_processing.*
|
||||||
homeassistant.components.image_upload.*
|
|
||||||
homeassistant.components.imap.*
|
|
||||||
homeassistant.components.imgw_pib.*
|
|
||||||
homeassistant.components.input_button.*
|
homeassistant.components.input_button.*
|
||||||
homeassistant.components.input_select.*
|
homeassistant.components.input_select.*
|
||||||
homeassistant.components.input_text.*
|
|
||||||
homeassistant.components.integration.*
|
homeassistant.components.integration.*
|
||||||
homeassistant.components.intent.*
|
|
||||||
homeassistant.components.intent_script.*
|
|
||||||
homeassistant.components.ios.*
|
|
||||||
homeassistant.components.iotty.*
|
|
||||||
homeassistant.components.ipp.*
|
|
||||||
homeassistant.components.iqvia.*
|
|
||||||
homeassistant.components.islamic_prayer_times.*
|
|
||||||
homeassistant.components.isy994.*
|
homeassistant.components.isy994.*
|
||||||
|
homeassistant.components.iqvia.*
|
||||||
homeassistant.components.jellyfin.*
|
homeassistant.components.jellyfin.*
|
||||||
homeassistant.components.jewish_calendar.*
|
homeassistant.components.jewish_calendar.*
|
||||||
homeassistant.components.jvc_projector.*
|
|
||||||
homeassistant.components.kaleidescape.*
|
|
||||||
homeassistant.components.knocki.*
|
|
||||||
homeassistant.components.knx.*
|
homeassistant.components.knx.*
|
||||||
homeassistant.components.kraken.*
|
homeassistant.components.kraken.*
|
||||||
homeassistant.components.lacrosse.*
|
|
||||||
homeassistant.components.lacrosse_view.*
|
|
||||||
homeassistant.components.lamarzocco.*
|
|
||||||
homeassistant.components.lametric.*
|
homeassistant.components.lametric.*
|
||||||
homeassistant.components.laundrify.*
|
|
||||||
homeassistant.components.lawn_mower.*
|
|
||||||
homeassistant.components.lcn.*
|
homeassistant.components.lcn.*
|
||||||
homeassistant.components.ld2410_ble.*
|
|
||||||
homeassistant.components.led_ble.*
|
|
||||||
homeassistant.components.lektrico.*
|
|
||||||
homeassistant.components.lidarr.*
|
|
||||||
homeassistant.components.lifx.*
|
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
homeassistant.components.linear_garage_door.*
|
|
||||||
homeassistant.components.linkplay.*
|
|
||||||
homeassistant.components.litejet.*
|
|
||||||
homeassistant.components.litterrobot.*
|
|
||||||
homeassistant.components.local_ip.*
|
homeassistant.components.local_ip.*
|
||||||
homeassistant.components.local_todo.*
|
|
||||||
homeassistant.components.lock.*
|
homeassistant.components.lock.*
|
||||||
homeassistant.components.logbook.*
|
|
||||||
homeassistant.components.logger.*
|
|
||||||
homeassistant.components.london_underground.*
|
|
||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.mailbox.*
|
||||||
homeassistant.components.manual.*
|
|
||||||
homeassistant.components.mastodon.*
|
|
||||||
homeassistant.components.matrix.*
|
|
||||||
homeassistant.components.matter.*
|
|
||||||
homeassistant.components.media_extractor.*
|
|
||||||
homeassistant.components.media_player.*
|
homeassistant.components.media_player.*
|
||||||
homeassistant.components.media_source.*
|
|
||||||
homeassistant.components.met_eireann.*
|
|
||||||
homeassistant.components.metoffice.*
|
|
||||||
homeassistant.components.mikrotik.*
|
|
||||||
homeassistant.components.min_max.*
|
|
||||||
homeassistant.components.minecraft_server.*
|
|
||||||
homeassistant.components.mjpeg.*
|
homeassistant.components.mjpeg.*
|
||||||
homeassistant.components.modbus.*
|
homeassistant.components.modbus.*
|
||||||
homeassistant.components.modem_callerid.*
|
homeassistant.components.modem_callerid.*
|
||||||
homeassistant.components.mold_indicator.*
|
homeassistant.components.media_source.*
|
||||||
homeassistant.components.monzo.*
|
|
||||||
homeassistant.components.moon.*
|
|
||||||
homeassistant.components.mopeka.*
|
|
||||||
homeassistant.components.motionmount.*
|
|
||||||
homeassistant.components.mqtt.*
|
|
||||||
homeassistant.components.music_assistant.*
|
|
||||||
homeassistant.components.my.*
|
|
||||||
homeassistant.components.mysensors.*
|
homeassistant.components.mysensors.*
|
||||||
homeassistant.components.myuplink.*
|
|
||||||
homeassistant.components.nam.*
|
homeassistant.components.nam.*
|
||||||
homeassistant.components.nanoleaf.*
|
homeassistant.components.nanoleaf.*
|
||||||
homeassistant.components.nasweb.*
|
|
||||||
homeassistant.components.neato.*
|
homeassistant.components.neato.*
|
||||||
homeassistant.components.nest.*
|
homeassistant.components.nest.*
|
||||||
homeassistant.components.netatmo.*
|
homeassistant.components.netatmo.*
|
||||||
homeassistant.components.network.*
|
homeassistant.components.network.*
|
||||||
homeassistant.components.nextdns.*
|
|
||||||
homeassistant.components.nfandroidtv.*
|
homeassistant.components.nfandroidtv.*
|
||||||
homeassistant.components.nightscout.*
|
|
||||||
homeassistant.components.nissan_leaf.*
|
homeassistant.components.nissan_leaf.*
|
||||||
homeassistant.components.no_ip.*
|
homeassistant.components.no_ip.*
|
||||||
homeassistant.components.nordpool.*
|
|
||||||
homeassistant.components.notify.*
|
homeassistant.components.notify.*
|
||||||
homeassistant.components.notion.*
|
homeassistant.components.notion.*
|
||||||
homeassistant.components.number.*
|
homeassistant.components.number.*
|
||||||
homeassistant.components.nut.*
|
|
||||||
homeassistant.components.onboarding.*
|
|
||||||
homeassistant.components.oncue.*
|
homeassistant.components.oncue.*
|
||||||
homeassistant.components.onewire.*
|
homeassistant.components.onewire.*
|
||||||
homeassistant.components.onkyo.*
|
|
||||||
homeassistant.components.open_meteo.*
|
homeassistant.components.open_meteo.*
|
||||||
homeassistant.components.openai_conversation.*
|
|
||||||
homeassistant.components.openexchangerates.*
|
|
||||||
homeassistant.components.opensky.*
|
|
||||||
homeassistant.components.openuv.*
|
homeassistant.components.openuv.*
|
||||||
homeassistant.components.oralb.*
|
|
||||||
homeassistant.components.otbr.*
|
|
||||||
homeassistant.components.overkiz.*
|
homeassistant.components.overkiz.*
|
||||||
homeassistant.components.p1_monitor.*
|
|
||||||
homeassistant.components.panel_custom.*
|
|
||||||
homeassistant.components.peco.*
|
|
||||||
homeassistant.components.persistent_notification.*
|
homeassistant.components.persistent_notification.*
|
||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
homeassistant.components.ping.*
|
|
||||||
homeassistant.components.plugwise.*
|
|
||||||
homeassistant.components.powerwall.*
|
|
||||||
homeassistant.components.private_ble_device.*
|
|
||||||
homeassistant.components.prometheus.*
|
|
||||||
homeassistant.components.proximity.*
|
homeassistant.components.proximity.*
|
||||||
homeassistant.components.prusalink.*
|
|
||||||
homeassistant.components.pure_energie.*
|
|
||||||
homeassistant.components.purpleair.*
|
|
||||||
homeassistant.components.pushbullet.*
|
|
||||||
homeassistant.components.pvoutput.*
|
homeassistant.components.pvoutput.*
|
||||||
homeassistant.components.qnap_qsw.*
|
|
||||||
homeassistant.components.rabbitair.*
|
|
||||||
homeassistant.components.radarr.*
|
|
||||||
homeassistant.components.radio_browser.*
|
|
||||||
homeassistant.components.rainforest_raven.*
|
|
||||||
homeassistant.components.rainmachine.*
|
homeassistant.components.rainmachine.*
|
||||||
homeassistant.components.raspberry_pi.*
|
|
||||||
homeassistant.components.rdw.*
|
homeassistant.components.rdw.*
|
||||||
homeassistant.components.recollect_waste.*
|
homeassistant.components.recollect_waste.*
|
||||||
homeassistant.components.recorder.*
|
homeassistant.components.recorder.purge
|
||||||
|
homeassistant.components.recorder.repack
|
||||||
|
homeassistant.components.recorder.statistics
|
||||||
homeassistant.components.remote.*
|
homeassistant.components.remote.*
|
||||||
homeassistant.components.renault.*
|
homeassistant.components.renault.*
|
||||||
homeassistant.components.repairs.*
|
|
||||||
homeassistant.components.rest.*
|
|
||||||
homeassistant.components.rest_command.*
|
|
||||||
homeassistant.components.rfxtrx.*
|
|
||||||
homeassistant.components.rhasspy.*
|
|
||||||
homeassistant.components.ridwell.*
|
homeassistant.components.ridwell.*
|
||||||
homeassistant.components.ring.*
|
|
||||||
homeassistant.components.rituals_perfume_genie.*
|
homeassistant.components.rituals_perfume_genie.*
|
||||||
homeassistant.components.roborock.*
|
|
||||||
homeassistant.components.roku.*
|
homeassistant.components.roku.*
|
||||||
homeassistant.components.romy.*
|
|
||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
homeassistant.components.rss_feed_template.*
|
|
||||||
homeassistant.components.rtsp_to_webrtc.*
|
homeassistant.components.rtsp_to_webrtc.*
|
||||||
homeassistant.components.ruuvi_gateway.*
|
|
||||||
homeassistant.components.ruuvitag_ble.*
|
|
||||||
homeassistant.components.samsungtv.*
|
homeassistant.components.samsungtv.*
|
||||||
homeassistant.components.scene.*
|
homeassistant.components.scene.*
|
||||||
homeassistant.components.schedule.*
|
|
||||||
homeassistant.components.scrape.*
|
|
||||||
homeassistant.components.script.*
|
|
||||||
homeassistant.components.search.*
|
|
||||||
homeassistant.components.select.*
|
homeassistant.components.select.*
|
||||||
homeassistant.components.sensibo.*
|
|
||||||
homeassistant.components.sensirion_ble.*
|
|
||||||
homeassistant.components.sensor.*
|
homeassistant.components.sensor.*
|
||||||
homeassistant.components.sensoterra.*
|
homeassistant.components.senseme.*
|
||||||
homeassistant.components.senz.*
|
|
||||||
homeassistant.components.sfr_box.*
|
|
||||||
homeassistant.components.shell_command.*
|
|
||||||
homeassistant.components.shelly.*
|
homeassistant.components.shelly.*
|
||||||
homeassistant.components.shopping_list.*
|
|
||||||
homeassistant.components.simplepush.*
|
|
||||||
homeassistant.components.simplisafe.*
|
homeassistant.components.simplisafe.*
|
||||||
homeassistant.components.siren.*
|
|
||||||
homeassistant.components.skybell.*
|
|
||||||
homeassistant.components.slack.*
|
homeassistant.components.slack.*
|
||||||
homeassistant.components.sleepiq.*
|
homeassistant.components.sleepiq.*
|
||||||
homeassistant.components.smhi.*
|
homeassistant.components.smhi.*
|
||||||
homeassistant.components.smlight.*
|
|
||||||
homeassistant.components.snooz.*
|
|
||||||
homeassistant.components.solarlog.*
|
|
||||||
homeassistant.components.sonarr.*
|
|
||||||
homeassistant.components.speedtestdotnet.*
|
|
||||||
homeassistant.components.spotify.*
|
|
||||||
homeassistant.components.sql.*
|
|
||||||
homeassistant.components.squeezebox.*
|
|
||||||
homeassistant.components.ssdp.*
|
homeassistant.components.ssdp.*
|
||||||
homeassistant.components.starlink.*
|
homeassistant.components.stookalert.*
|
||||||
homeassistant.components.statistics.*
|
homeassistant.components.statistics.*
|
||||||
homeassistant.components.steamist.*
|
homeassistant.components.steamist.*
|
||||||
homeassistant.components.stookalert.*
|
|
||||||
homeassistant.components.stream.*
|
homeassistant.components.stream.*
|
||||||
homeassistant.components.streamlabswater.*
|
|
||||||
homeassistant.components.stt.*
|
|
||||||
homeassistant.components.suez_water.*
|
|
||||||
homeassistant.components.sun.*
|
homeassistant.components.sun.*
|
||||||
homeassistant.components.surepetcare.*
|
homeassistant.components.surepetcare.*
|
||||||
homeassistant.components.switch.*
|
homeassistant.components.switch.*
|
||||||
homeassistant.components.switch_as_x.*
|
|
||||||
homeassistant.components.switchbee.*
|
|
||||||
homeassistant.components.switchbot_cloud.*
|
|
||||||
homeassistant.components.switcher_kis.*
|
homeassistant.components.switcher_kis.*
|
||||||
homeassistant.components.synology_dsm.*
|
homeassistant.components.synology_dsm.*
|
||||||
homeassistant.components.system_health.*
|
|
||||||
homeassistant.components.system_log.*
|
|
||||||
homeassistant.components.systemmonitor.*
|
homeassistant.components.systemmonitor.*
|
||||||
homeassistant.components.tag.*
|
homeassistant.components.tag.*
|
||||||
homeassistant.components.tailscale.*
|
homeassistant.components.tailscale.*
|
||||||
homeassistant.components.tailwind.*
|
|
||||||
homeassistant.components.tami4.*
|
|
||||||
homeassistant.components.tautulli.*
|
homeassistant.components.tautulli.*
|
||||||
homeassistant.components.tcp.*
|
homeassistant.components.tcp.*
|
||||||
homeassistant.components.technove.*
|
|
||||||
homeassistant.components.tedee.*
|
|
||||||
homeassistant.components.text.*
|
|
||||||
homeassistant.components.thethingsnetwork.*
|
|
||||||
homeassistant.components.threshold.*
|
|
||||||
homeassistant.components.tibber.*
|
|
||||||
homeassistant.components.tile.*
|
homeassistant.components.tile.*
|
||||||
homeassistant.components.tilt_ble.*
|
|
||||||
homeassistant.components.time.*
|
|
||||||
homeassistant.components.time_date.*
|
|
||||||
homeassistant.components.timer.*
|
|
||||||
homeassistant.components.tod.*
|
|
||||||
homeassistant.components.todo.*
|
|
||||||
homeassistant.components.tolo.*
|
|
||||||
homeassistant.components.tplink.*
|
homeassistant.components.tplink.*
|
||||||
homeassistant.components.tplink_omada.*
|
homeassistant.components.tolo.*
|
||||||
homeassistant.components.trace.*
|
|
||||||
homeassistant.components.tractive.*
|
homeassistant.components.tractive.*
|
||||||
homeassistant.components.tradfri.*
|
homeassistant.components.tradfri.*
|
||||||
homeassistant.components.trafikverket_camera.*
|
|
||||||
homeassistant.components.trafikverket_ferry.*
|
|
||||||
homeassistant.components.trafikverket_train.*
|
homeassistant.components.trafikverket_train.*
|
||||||
homeassistant.components.trafikverket_weatherstation.*
|
homeassistant.components.trafikverket_weatherstation.*
|
||||||
homeassistant.components.transmission.*
|
|
||||||
homeassistant.components.trend.*
|
|
||||||
homeassistant.components.tts.*
|
homeassistant.components.tts.*
|
||||||
homeassistant.components.twentemilieu.*
|
homeassistant.components.twentemilieu.*
|
||||||
homeassistant.components.unifi.*
|
|
||||||
homeassistant.components.unifiprotect.*
|
homeassistant.components.unifiprotect.*
|
||||||
homeassistant.components.upcloud.*
|
homeassistant.components.upcloud.*
|
||||||
homeassistant.components.update.*
|
|
||||||
homeassistant.components.uptime.*
|
homeassistant.components.uptime.*
|
||||||
homeassistant.components.uptimerobot.*
|
homeassistant.components.uptimerobot.*
|
||||||
homeassistant.components.usb.*
|
|
||||||
homeassistant.components.uvc.*
|
|
||||||
homeassistant.components.vacuum.*
|
homeassistant.components.vacuum.*
|
||||||
homeassistant.components.vallox.*
|
homeassistant.components.vallox.*
|
||||||
homeassistant.components.valve.*
|
|
||||||
homeassistant.components.velbus.*
|
homeassistant.components.velbus.*
|
||||||
homeassistant.components.vlc_telnet.*
|
homeassistant.components.vlc_telnet.*
|
||||||
homeassistant.components.wake_on_lan.*
|
|
||||||
homeassistant.components.wake_word.*
|
|
||||||
homeassistant.components.wallbox.*
|
homeassistant.components.wallbox.*
|
||||||
homeassistant.components.waqi.*
|
|
||||||
homeassistant.components.water_heater.*
|
homeassistant.components.water_heater.*
|
||||||
homeassistant.components.watttime.*
|
homeassistant.components.watttime.*
|
||||||
homeassistant.components.weather.*
|
homeassistant.components.weather.*
|
||||||
homeassistant.components.webhook.*
|
|
||||||
homeassistant.components.webostv.*
|
homeassistant.components.webostv.*
|
||||||
homeassistant.components.websocket_api.*
|
homeassistant.components.websocket_api.*
|
||||||
homeassistant.components.wemo.*
|
homeassistant.components.wemo.*
|
||||||
homeassistant.components.whois.*
|
homeassistant.components.whois.*
|
||||||
homeassistant.components.withings.*
|
|
||||||
homeassistant.components.wiz.*
|
homeassistant.components.wiz.*
|
||||||
homeassistant.components.wled.*
|
|
||||||
homeassistant.components.workday.*
|
|
||||||
homeassistant.components.worldclock.*
|
|
||||||
homeassistant.components.xiaomi_ble.*
|
|
||||||
homeassistant.components.yale_smart_alarm.*
|
|
||||||
homeassistant.components.yalexs_ble.*
|
|
||||||
homeassistant.components.youtube.*
|
|
||||||
homeassistant.components.zeroconf.*
|
|
||||||
homeassistant.components.zodiac.*
|
homeassistant.components.zodiac.*
|
||||||
|
homeassistant.components.zeroconf.*
|
||||||
homeassistant.components.zone.*
|
homeassistant.components.zone.*
|
||||||
homeassistant.components.zwave_js.*
|
homeassistant.components.zwave_js.*
|
||||||
|
|
6
.vscode/extensions.json
vendored
|
@ -1,7 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["esbenp.prettier-vscode", "ms-python.python"]
|
||||||
"charliermarsh.ruff",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"ms-python.python"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
50
.vscode/launch.json
vendored
|
@ -6,52 +6,20 @@
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Home Assistant",
|
"name": "Home Assistant",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "homeassistant",
|
"module": "homeassistant",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": [
|
"args": ["--debug", "-c", "config"]
|
||||||
"--debug",
|
|
||||||
"-c",
|
|
||||||
"config"
|
|
||||||
],
|
|
||||||
"preLaunchTask": "Compile English translations"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Home Assistant (skip pip)",
|
// Debug by attaching to local Home Asistant server using Remote Python Debugger.
|
||||||
"type": "debugpy",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "homeassistant",
|
|
||||||
"justMyCode": false,
|
|
||||||
"args": [
|
|
||||||
"--debug",
|
|
||||||
"-c",
|
|
||||||
"config",
|
|
||||||
"--skip-pip"
|
|
||||||
],
|
|
||||||
"preLaunchTask": "Compile English translations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Home Assistant: Changed tests",
|
|
||||||
"type": "debugpy",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "pytest",
|
|
||||||
"justMyCode": false,
|
|
||||||
"args": [
|
|
||||||
"--timeout=10",
|
|
||||||
"--picked"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
|
||||||
// See https://www.home-assistant.io/integrations/debugpy/
|
// See https://www.home-assistant.io/integrations/debugpy/
|
||||||
"name": "Home Assistant: Attach Local",
|
"name": "Home Assistant: Attach Local",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
|
||||||
"port": 5678,
|
"port": 5678,
|
||||||
"host": "localhost"
|
"host": "localhost",
|
||||||
},
|
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
|
@ -60,15 +28,13 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
|
// Debug by attaching to remote Home Asistant server using Remote Python Debugger.
|
||||||
// See https://www.home-assistant.io/integrations/debugpy/
|
// See https://www.home-assistant.io/integrations/debugpy/
|
||||||
"name": "Home Assistant: Attach Remote",
|
"name": "Home Assistant: Attach Remote",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
|
||||||
"port": 5678,
|
"port": 5678,
|
||||||
"host": "homeassistant.local"
|
"host": "homeassistant.local",
|
||||||
},
|
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
|
|
13
.vscode/settings.default.json
vendored
|
@ -1,18 +1,9 @@
|
||||||
{
|
{
|
||||||
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
||||||
|
"python.formatting.provider": "black",
|
||||||
// Added --no-cov to work around TypeError: message must be set
|
// Added --no-cov to work around TypeError: message must be set
|
||||||
// https://github.com/microsoft/vscode-python/issues/14067
|
// https://github.com/microsoft/vscode-python/issues/14067
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false
|
||||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
|
||||||
"pylint.importStrategy": "fromEnvironment",
|
|
||||||
"json.schemas": [
|
|
||||||
{
|
|
||||||
"fileMatch": [
|
|
||||||
"homeassistant/components/*/manifest.json"
|
|
||||||
],
|
|
||||||
"url": "./script/json_schemas/manifest_schema.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
84
.vscode/tasks.json
vendored
|
@ -10,13 +10,12 @@
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": [],
|
"problemMatcher": []
|
||||||
"dependsOn": ["Compile English translations"]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pytest",
|
"label": "Pytest",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest --timeout=10 tests",
|
"command": "pytest --timeout=10 tests",
|
||||||
"dependsOn": ["Install all Test Requirements"],
|
"dependsOn": ["Install all Test Requirements"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
|
@ -29,23 +28,9 @@
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pytest (changed tests only)",
|
"label": "Flake8",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest --timeout=10 --picked",
|
"command": "pre-commit run flake8 --all-files",
|
||||||
"group": {
|
|
||||||
"kind": "test",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "new"
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ruff",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pre-commit run ruff --all-files",
|
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
@ -75,8 +60,7 @@
|
||||||
"label": "Code Coverage",
|
"label": "Code Coverage",
|
||||||
"detail": "Generate code coverage report for a given integration.",
|
"detail": "Generate code coverage report for a given integration.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
"command": "pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
||||||
"dependsOn": ["Compile English translations"],
|
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
@ -104,7 +88,7 @@
|
||||||
{
|
{
|
||||||
"label": "Install all Requirements",
|
"label": "Install all Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "uv pip install -r requirements_all.txt",
|
"command": "pip3 install --use-deprecated=legacy-resolver -r requirements_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
@ -118,7 +102,7 @@
|
||||||
{
|
{
|
||||||
"label": "Install all Test Requirements",
|
"label": "Install all Test Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "uv pip install -r requirements_test_all.txt",
|
"command": "pip3 install --use-deprecated=legacy-resolver -r requirements_test_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
@ -130,40 +114,10 @@
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Compile English translations",
|
"label": "Compile translations",
|
||||||
"detail": "In order to test changes to translation files, the translation strings must be compiled into Home Assistant's translation directories.",
|
"detail": "In order to test changes to translation files, the translation strings must be compiled into Home Assistant's translation directories.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m script.translations develop --all",
|
"command": "python3 -m script.translations develop --integration ${input:integrationName}",
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Run scaffold",
|
|
||||||
"detail": "Add new functionality to a integration using a scaffold.",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "python3 -m script.scaffold ${input:scaffoldName} --integration ${input:integrationName}",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Create new integration",
|
|
||||||
"detail": "Use the scaffold to create a new integration.",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "python3 -m script.scaffold integration",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Install integration requirements",
|
|
||||||
"detail": "Install all requirements of a given integration.",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "${command:python.interpreterPath} -m script.install_integration_requirements ${input:integrationName}",
|
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
@ -171,7 +125,8 @@
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
}
|
},
|
||||||
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
|
@ -179,23 +134,6 @@
|
||||||
"id": "integrationName",
|
"id": "integrationName",
|
||||||
"type": "promptString",
|
"type": "promptString",
|
||||||
"description": "For which integration should the task run?"
|
"description": "For which integration should the task run?"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "scaffoldName",
|
|
||||||
"type": "pickString",
|
|
||||||
"options": [
|
|
||||||
"backup",
|
|
||||||
"config_flow",
|
|
||||||
"config_flow_discovery",
|
|
||||||
"config_flow_helper",
|
|
||||||
"config_flow_oauth2",
|
|
||||||
"device_action",
|
|
||||||
"device_condition",
|
|
||||||
"device_trigger",
|
|
||||||
"reproduce_state",
|
|
||||||
"significant_change"
|
|
||||||
],
|
|
||||||
"description": "Which scaffold should be run?"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ignore: |
|
ignore: |
|
||||||
tests/fixtures/core/config/yaml_errors/
|
azure-*.yml
|
||||||
rules:
|
rules:
|
||||||
braces:
|
braces:
|
||||||
level: error
|
level: error
|
||||||
|
@ -25,7 +25,7 @@ rules:
|
||||||
comments:
|
comments:
|
||||||
level: error
|
level: error
|
||||||
require-starting-space: true
|
require-starting-space: true
|
||||||
min-spaces-from-content: 1
|
min-spaces-from-content: 2
|
||||||
comments-indentation:
|
comments-indentation:
|
||||||
level: error
|
level: error
|
||||||
document-end:
|
document-end:
|
||||||
|
|
2852
CODEOWNERS
|
@ -5,7 +5,7 @@
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socioeconomic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ enforcement ladder][mozilla].
|
||||||
|
|
||||||
## Adoption
|
## Adoption
|
||||||
|
|
||||||
This Code of Conduct was first adopted on January 21st, 2017, and announced in
|
This Code of Conduct was first adopted January 21st, 2017 and announced in
|
||||||
[this][coc-blog] blog post and has been updated on May 25th, 2020 to version
|
[this][coc-blog] blog post and has been updated on May 25th, 2020 to version
|
||||||
2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog]
|
2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog]
|
||||||
blog post.
|
blog post.
|
||||||
|
@ -132,8 +132,8 @@ For answers to common questions about this code of conduct, see the FAQ at
|
||||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||||
<https://www.contributor-covenant.org/translations>.
|
<https://www.contributor-covenant.org/translations>.
|
||||||
|
|
||||||
[coc-blog]: https://www.home-assistant.io/blog/2017/01/21/home-assistant-governance/
|
[coc-blog]: /blog/2017/01/21/home-assistant-governance/
|
||||||
[coc2-blog]: https://www.home-assistant.io/blog/2020/05/25/code-of-conduct-updated/
|
[coc2-blog]: /blog/2020/05/25/code-of-conduct-updated/
|
||||||
[email]: mailto:safety@home-assistant.io
|
[email]: mailto:safety@home-assistant.io
|
||||||
[homepage]: http://contributor-covenant.org
|
[homepage]: http://contributor-covenant.org
|
||||||
[mozilla]: https://github.com/mozilla/diversity
|
[mozilla]: https://github.com/mozilla/diversity
|
||||||
|
|
65
Dockerfile
|
@ -1,19 +1,9 @@
|
||||||
# Automatically generated by hassfest.
|
|
||||||
#
|
|
||||||
# To update, run python3 -m script.hassfest -p docker
|
|
||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
# Synchronize with homeassistant/core.py:async_stop
|
# Synchronize with homeassistant/core.py:async_stop
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=240000 \
|
S6_SERVICES_GRACETIME=220000
|
||||||
UV_SYSTEM_PYTHON=true \
|
|
||||||
UV_NO_CACHE=true
|
|
||||||
|
|
||||||
ARG QEMU_CPU
|
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN pip3 install uv==0.5.0
|
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@ -21,43 +11,36 @@ WORKDIR /usr/src
|
||||||
COPY requirements.txt homeassistant/
|
COPY requirements.txt homeassistant/
|
||||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||||
RUN \
|
RUN \
|
||||||
uv pip install \
|
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||||
--no-build \
|
-r homeassistant/requirements.txt --use-deprecated=legacy-resolver
|
||||||
-r homeassistant/requirements.txt
|
COPY requirements_all.txt homeassistant/
|
||||||
|
|
||||||
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
|
|
||||||
RUN \
|
RUN \
|
||||||
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
|
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||||
uv pip install homeassistant/home_assistant_*.whl; \
|
-r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver
|
||||||
fi \
|
|
||||||
&& uv pip install \
|
|
||||||
--no-build \
|
|
||||||
-r homeassistant/requirements_all.txt
|
|
||||||
|
|
||||||
## Setup Home Assistant Core
|
## Setup Home Assistant Core
|
||||||
COPY . homeassistant/
|
COPY . homeassistant/
|
||||||
RUN \
|
RUN \
|
||||||
uv pip install \
|
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||||
-e ./homeassistant \
|
-e ./homeassistant --use-deprecated=legacy-resolver \
|
||||||
&& python3 -m compileall \
|
&& python3 -m compileall homeassistant/homeassistant
|
||||||
homeassistant/homeassistant
|
|
||||||
|
# Fix Bug with Alpine 3.14 and sqlite 3.35
|
||||||
|
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
RUN \
|
||||||
|
if [ "${BUILD_ARCH}" = "amd64" ]; then \
|
||||||
|
export APK_ARCH=x86_64; \
|
||||||
|
elif [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||||
|
export APK_ARCH=x86; \
|
||||||
|
else \
|
||||||
|
export APK_ARCH=${BUILD_ARCH}; \
|
||||||
|
fi \
|
||||||
|
&& curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \
|
||||||
|
&& apk add --no-cache sqlite-libs-3.34.1-r0.apk \
|
||||||
|
&& rm -f sqlite-libs-3.34.1-r0.apk
|
||||||
|
|
||||||
# Home Assistant S6-Overlay
|
# Home Assistant S6-Overlay
|
||||||
COPY rootfs /
|
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.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
|
||||||
&& chmod +x /bin/go2rtc \
|
|
||||||
# Verify go2rtc can be executed
|
|
||||||
&& go2rtc --version
|
|
||||||
|
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
|
|
|
@ -1,68 +1,42 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/python:1-3.12
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
# Uninstall pre-installed formatting and linting tools
|
|
||||||
# They would conflict with our pinned versions
|
|
||||||
RUN \
|
|
||||||
pipx uninstall pydocstyle \
|
|
||||||
&& pipx uninstall pycodestyle \
|
|
||||||
&& pipx uninstall mypy \
|
|
||||||
&& pipx uninstall pylint
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
|
||||||
libudev-dev \
|
libudev-dev \
|
||||||
libavformat-dev \
|
libavformat-dev \
|
||||||
libavcodec-dev \
|
libavcodec-dev \
|
||||||
libavdevice-dev \
|
libavdevice-dev \
|
||||||
libavutil-dev \
|
libavutil-dev \
|
||||||
libgammu-dev \
|
|
||||||
libswscale-dev \
|
libswscale-dev \
|
||||||
libswresample-dev \
|
libswresample-dev \
|
||||||
libavfilter-dev \
|
libavfilter-dev \
|
||||||
libpcap-dev \
|
libpcap-dev \
|
||||||
libturbojpeg0 \
|
libturbojpeg0 \
|
||||||
libyaml-dev \
|
|
||||||
libxml2 \
|
|
||||||
git \
|
git \
|
||||||
cmake \
|
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add go2rtc binary
|
|
||||||
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN pip3 install uv
|
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||||
&& uv pip install --system -e hass-release/ \
|
&& pip3 install -e hass-release/
|
||||||
&& chown -R vscode /usr/src/hass-release/data
|
|
||||||
|
|
||||||
USER vscode
|
WORKDIR /workspaces
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
|
||||||
RUN uv venv $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
RUN uv pip install -r requirements.txt
|
RUN pip3 install -r requirements.txt --use-deprecated=legacy-resolver
|
||||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||||
RUN uv pip install -r requirements_test.txt
|
RUN pip3 install -r requirements_test.txt --use-deprecated=legacy-resolver
|
||||||
|
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||||
WORKDIR /workspaces
|
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
# Set the default shell to bash instead of sh
|
||||||
ENV SHELL /bin/bash
|
ENV SHELL /bin/bash
|
||||||
|
|
17
README.rst
|
@ -4,7 +4,7 @@ Home Assistant |Chat Status|
|
||||||
Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.
|
Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.
|
||||||
|
|
||||||
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||||
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||||
|
|
||||||
|screenshot-states|
|
|screenshot-states|
|
||||||
|
@ -12,7 +12,7 @@ demo <https://demo.home-assistant.io>`__, `installation instructions <https://ho
|
||||||
Featured integrations
|
Featured integrations
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|screenshot-integrations|
|
|screenshot-components|
|
||||||
|
|
||||||
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://developers.home-assistant.io/docs/architecture_index/>`__ and the `section on creating your own
|
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://developers.home-assistant.io/docs/architecture_index/>`__ and the `section on creating your own
|
||||||
components <https://developers.home-assistant.io/docs/creating_component_index/>`__.
|
components <https://developers.home-assistant.io/docs/creating_component_index/>`__.
|
||||||
|
@ -20,14 +20,9 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
|
||||||
If you run into issues while using Home Assistant or during development
|
If you run into issues while using Home Assistant or during development
|
||||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||||
|
|
||||||
|ohf-logo|
|
|
||||||
|
|
||||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||||
:target: https://www.home-assistant.io/join-chat/
|
:target: https://discord.gg/c5DvZ4e
|
||||||
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||||
:target: https://demo.home-assistant.io
|
:target: https://home-assistant.io/demo/
|
||||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||||
:target: https://home-assistant.io/integrations/
|
:target: https://home-assistant.io/integrations/
|
||||||
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
|
|
||||||
:alt: Home Assistant - A project from the Open Home Foundation
|
|
||||||
:target: https://www.openhomefoundation.org/
|
|
||||||
|
|
16
build.yaml
|
@ -1,16 +1,14 @@
|
||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: homeassistant/{arch}-homeassistant
|
||||||
|
shadow_repository: ghcr.io/home-assistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.02.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.02.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.02.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.02.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.02.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
cosign:
|
|
||||||
base_identity: https://github.com/home-assistant/docker/.*
|
|
||||||
identity: https://github.com/home-assistant/core/.*
|
|
||||||
labels:
|
labels:
|
||||||
io.hass.type: core
|
io.hass.type: core
|
||||||
org.opencontainers.image.title: Home Assistant
|
org.opencontainers.image.title: Home Assistant
|
||||||
|
|
26
codecov.yml
|
@ -4,41 +4,21 @@ coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: auto
|
target: 90
|
||||||
threshold: 0.09
|
threshold: 0.09
|
||||||
required:
|
config-flows:
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 1
|
threshold: 1
|
||||||
paths:
|
paths:
|
||||||
- homeassistant/components/*/config_flow.py
|
- homeassistant/components/*/config_flow.py
|
||||||
- homeassistant/components/*/device_action.py
|
|
||||||
- homeassistant/components/*/device_condition.py
|
|
||||||
- homeassistant/components/*/device_trigger.py
|
|
||||||
- homeassistant/components/*/diagnostics.py
|
|
||||||
- homeassistant/components/*/group.py
|
|
||||||
- homeassistant/components/*/intent.py
|
|
||||||
- homeassistant/components/*/logbook.py
|
|
||||||
- homeassistant/components/*/media_source.py
|
|
||||||
- homeassistant/components/*/recorder.py
|
|
||||||
- homeassistant/components/*/scene.py
|
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: auto
|
target: auto
|
||||||
required:
|
config-flows:
|
||||||
target: 100
|
target: 100
|
||||||
threshold: 0
|
threshold: 0
|
||||||
paths:
|
paths:
|
||||||
- homeassistant/components/*/config_flow.py
|
- homeassistant/components/*/config_flow.py
|
||||||
- homeassistant/components/*/device_action.py
|
|
||||||
- homeassistant/components/*/device_condition.py
|
|
||||||
- homeassistant/components/*/device_trigger.py
|
|
||||||
- homeassistant/components/*/diagnostics.py
|
|
||||||
- homeassistant/components/*/group.py
|
|
||||||
- homeassistant/components/*/intent.py
|
|
||||||
- homeassistant/components/*/logbook.py
|
|
||||||
- homeassistant/components/*/media_source.py
|
|
||||||
- homeassistant/components/*/recorder.py
|
|
||||||
- homeassistant/components/*/scene.py
|
|
||||||
comment: false
|
comment: false
|
||||||
|
|
||||||
# To make partial tests possible,
|
# To make partial tests possible,
|
||||||
|
|
230
docs/Makefile
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " livehtml to make standalone HTML files via sphinx-autobuild"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " applehelp to make an Apple Help Book"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " epub3 to make an epub3"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " xml to make Docutils-native XML files"
|
||||||
|
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||||
|
@echo " dummy to check syntax errors of document sources"
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
.PHONY: html
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
.PHONY: livehtml
|
||||||
|
livehtml:
|
||||||
|
sphinx-autobuild -z ../homeassistant/ --port 0 -B -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
|
||||||
|
.PHONY: dirhtml
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
.PHONY: singlehtml
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
.PHONY: pickle
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
.PHONY: json
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
.PHONY: htmlhelp
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
.PHONY: qthelp
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Home-Assistant.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Home-Assistant.qhc"
|
||||||
|
|
||||||
|
.PHONY: applehelp
|
||||||
|
applehelp:
|
||||||
|
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||||
|
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||||
|
"~/Library/Documentation/Help or install it in your application" \
|
||||||
|
"bundle."
|
||||||
|
|
||||||
|
.PHONY: devhelp
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/Home-Assistant"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Home-Assistant"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
.PHONY: epub
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
.PHONY: epub3
|
||||||
|
epub3:
|
||||||
|
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||||
|
|
||||||
|
.PHONY: latex
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: latexpdf
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: latexpdfja
|
||||||
|
latexpdfja:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: text
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
.PHONY: man
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
.PHONY: texinfo
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: info
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
.PHONY: gettext
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
.PHONY: changes
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
.PHONY: linkcheck
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
.PHONY: doctest
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage:
|
||||||
|
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||||
|
@echo "Testing of coverage in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/coverage/python.txt."
|
||||||
|
|
||||||
|
.PHONY: xml
|
||||||
|
xml:
|
||||||
|
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||||
|
|
||||||
|
.PHONY: pseudoxml
|
||||||
|
pseudoxml:
|
||||||
|
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||||
|
|
||||||
|
.PHONY: dummy
|
||||||
|
dummy:
|
||||||
|
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. Dummy builder generates no files."
|
0
homeassistant/py.typed → docs/build/.empty
vendored
281
docs/make.bat
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set BUILDDIR=build
|
||||||
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% source
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. epub3 to make an epub3
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. xml to make Docutils-native XML files
|
||||||
|
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
echo. coverage to run coverage check of the documentation if enabled
|
||||||
|
echo. dummy to check syntax errors of document sources
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REM Check if sphinx-build is available and fallback to Python version if any
|
||||||
|
%SPHINXBUILD% 1>NUL 2>NUL
|
||||||
|
if errorlevel 9009 goto sphinx_python
|
||||||
|
goto sphinx_ok
|
||||||
|
|
||||||
|
:sphinx_python
|
||||||
|
|
||||||
|
set SPHINXBUILD=python -m sphinx.__init__
|
||||||
|
%SPHINXBUILD% 2> nul
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:sphinx_ok
|
||||||
|
|
||||||
|
|
||||||
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Home-Assistant.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Home-Assistant.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub3" (
|
||||||
|
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdf" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdfja" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf-ja
|
||||||
|
cd %~dp0
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "coverage" (
|
||||||
|
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of coverage in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/coverage/python.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "xml" (
|
||||||
|
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pseudoxml" (
|
||||||
|
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dummy" (
|
||||||
|
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. Dummy builder generates no files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
:end
|
BIN
docs/screenshot-components.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
docs/screenshots.png
Normal file
After Width: | Height: | Size: 226 KiB |
44
docs/source/_ext/edit_on_github.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
"""
|
||||||
|
Sphinx extension to add ReadTheDocs-style "Edit on GitHub" links to the
|
||||||
|
sidebar.
|
||||||
|
|
||||||
|
Loosely based on https://github.com/astropy/astropy/pull/347
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
__licence__ = "BSD (3 clause)"
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_url(app, view, path):
|
||||||
|
return (
|
||||||
|
f"https://github.com/{app.config.edit_on_github_project}/"
|
||||||
|
f"{view}/{app.config.edit_on_github_branch}/"
|
||||||
|
f"{app.config.edit_on_github_src_path}{path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def html_page_context(app, pagename, templatename, context, doctree):
|
||||||
|
if templatename != "page.html":
|
||||||
|
return
|
||||||
|
|
||||||
|
if not app.config.edit_on_github_project:
|
||||||
|
warnings.warn("edit_on_github_project not specified")
|
||||||
|
return
|
||||||
|
if not doctree:
|
||||||
|
warnings.warn("doctree is None")
|
||||||
|
return
|
||||||
|
path = os.path.relpath(doctree.get("source"), app.builder.srcdir)
|
||||||
|
show_url = get_github_url(app, "blob", path)
|
||||||
|
edit_url = get_github_url(app, "edit", path)
|
||||||
|
|
||||||
|
context["show_on_github_url"] = show_url
|
||||||
|
context["edit_on_github_url"] = edit_url
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.add_config_value("edit_on_github_project", "", True)
|
||||||
|
app.add_config_value("edit_on_github_branch", "master", True)
|
||||||
|
app.add_config_value("edit_on_github_src_path", "", True) # 'eg' "docs/"
|
||||||
|
app.connect("html-page-context", html_page_context)
|
BIN
docs/source/_static/favicon.ico
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
docs/source/_static/logo-apple.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/source/_static/logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
6
docs/source/_templates/links.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://home-assistant.io/">Homepage</a></li>
|
||||||
|
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
|
||||||
|
<li><a href="https://github.com/home-assistant/core">GitHub</a></li>
|
||||||
|
<li><a href="https://discord.gg/c5DvZ4e">Discord</a></li>
|
||||||
|
</ul>
|
13
docs/source/_templates/sourcelink.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{%- if show_source and has_source and sourcename %}
|
||||||
|
<h3>{{ _('This Page') }}</h3>
|
||||||
|
<ul class="this-page-menu">
|
||||||
|
{%- if show_on_github_url %}
|
||||||
|
<li><a href="{{ show_on_github_url }}"
|
||||||
|
rel="nofollow">{{ _('Show on GitHub') }}</a></li>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if edit_on_github_url %}
|
||||||
|
<li><a href="{{ edit_on_github_url }}"
|
||||||
|
rel="nofollow">{{ _('Edit on GitHub') }}</a></li>
|
||||||
|
{%- endif %}
|
||||||
|
</ul>
|
||||||
|
{%- endif %}
|
29
docs/source/api/auth.rst
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
:mod:`homeassistant.auth`
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.auth
|
||||||
|
:members:
|
||||||
|
|
||||||
|
homeassistant.auth.auth\_store
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.auth.auth_store
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.auth.const
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.auth.const
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.auth.models
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.auth.models
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
7
docs/source/api/bootstrap.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _bootstrap_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.bootstrap`
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.bootstrap
|
||||||
|
:members:
|
170
docs/source/api/components.rst
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
:mod:`homeassistant.components`
|
||||||
|
===============================
|
||||||
|
|
||||||
|
air\_quality
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.air_quality
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
alarm\_control\_panel
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.alarm_control_panel
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
binary\_sensor
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.binary_sensor
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
camera
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.camera
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
calendar
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.calendar
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
climate
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.climate
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
conversation
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.conversation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
cover
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.cover
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
device\_tracker
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.device_tracker
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
fan
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.fan
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
light
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.light
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
lock
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.lock
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
media\_player
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.media_player
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
notify
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.notify
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
remote
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.remote
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
switch
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.switch
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
sensor
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.sensor
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
vacuum
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.vacuum
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
water\_heater
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.water_heater
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
weather
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.weather
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
webhook
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.components.webhook
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
7
docs/source/api/config_entries.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _config_entries_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.config_entries`
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.config_entries
|
||||||
|
:members:
|
7
docs/source/api/core.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _core_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.core`
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.core
|
||||||
|
:members:
|
7
docs/source/api/data_entry_flow.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _data_entry_flow_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.data_entry_flow`
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.data_entry_flow
|
||||||
|
:members:
|
7
docs/source/api/exceptions.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _exceptions_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.exceptions`
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.exceptions
|
||||||
|
:members:
|
335
docs/source/api/helpers.rst
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
:mod:`homeassistant.helpers`
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.aiohttp\_client
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.aiohttp_client
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.area\_registry
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.area_registry
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.check\_config
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.check_config
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.collection
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.collection
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.condition
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.condition
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.config\_entry\_flow
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.config_entry_flow
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.config\_entry\_oauth2\_flow
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.config_entry_oauth2_flow
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.config\_validation
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.config_validation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.data\_entry\_flow
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.data_entry_flow
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.debounce
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.debounce
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.deprecation
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.deprecation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.device\_registry
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.device_registry
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.discovery
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.discovery
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.dispatcher
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.dispatcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entity
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entity
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entity\_component
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entity_component
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entity\_platform
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entity_platform
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entity\_registry
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entity_registry
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entity\_values
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entity_values
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.entityfilter
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.entityfilter
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.event
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.event
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.icon
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.icon
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.integration\_platform
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.integration_platform
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.intent
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.intent
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.json
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.json
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.location
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.location
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.logging
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.logging
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.network
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.network
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.restore\_state
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.restore_state
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.script
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.script
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.service
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.service
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.signal
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.signal
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.state
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.state
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.storage
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.storage
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.sun
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.sun
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.system\_info
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.system_info
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.temperature
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.temperature
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.template
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.template
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.translation
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.translation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.typing
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.typing
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.helpers.update\_coordinator
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.helpers.update_coordinator
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
7
docs/source/api/loader.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.. _loader_module:
|
||||||
|
|
||||||
|
:mod:`homeassistant.loader`
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.loader
|
||||||
|
:members:
|
151
docs/source/api/util.rst
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
:mod:`homeassistant.util`
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.yaml
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.yaml
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.aiohttp
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.aiohttp
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.async\_
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.async_
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.color
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.color
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.decorator
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.decorator
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.distance
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.distance
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.dt
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.dt
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.json
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.json
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.location
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.location
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.logging
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.logging
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.network
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.network
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.package
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.package
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.pil
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.pil
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.pressure
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.pressure
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.ssl
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.ssl
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.temperature
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.temperature
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.unit\_system
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.unit_system
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
homeassistant.util.volume
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: homeassistant.util.volume
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
439
docs/source/conf.py
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Home-Assistant documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Sun Aug 28 13:13:10 2016.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from homeassistant.const import __short_version__, __version__
|
||||||
|
|
||||||
|
PROJECT_NAME = "Home Assistant"
|
||||||
|
PROJECT_PACKAGE_NAME = "homeassistant"
|
||||||
|
PROJECT_AUTHOR = "The Home Assistant Authors"
|
||||||
|
PROJECT_COPYRIGHT = f" 2013-2020, {PROJECT_AUTHOR}"
|
||||||
|
PROJECT_LONG_DESCRIPTION = (
|
||||||
|
"Home Assistant is an open-source "
|
||||||
|
"home automation platform running on Python 3. "
|
||||||
|
"Track and control all devices at home and "
|
||||||
|
"automate control. "
|
||||||
|
"Installation in less than a minute."
|
||||||
|
)
|
||||||
|
PROJECT_GITHUB_USERNAME = "home-assistant"
|
||||||
|
PROJECT_GITHUB_REPOSITORY = "home-assistant"
|
||||||
|
|
||||||
|
GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}"
|
||||||
|
GITHUB_URL = f"https://github.com/{GITHUB_PATH}"
|
||||||
|
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath("_ext"))
|
||||||
|
sys.path.insert(0, os.path.abspath("../homeassistant"))
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.linkcode",
|
||||||
|
"sphinx_autodoc_annotation",
|
||||||
|
"edit_on_github",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = ".rst"
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#
|
||||||
|
# source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = "index"
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = PROJECT_NAME
|
||||||
|
copyright = PROJECT_COPYRIGHT
|
||||||
|
author = PROJECT_AUTHOR
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = __short_version__
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = __version__
|
||||||
|
|
||||||
|
code_branch = "dev" if "dev" in __version__ else "master"
|
||||||
|
|
||||||
|
# Edit on Github config
|
||||||
|
edit_on_github_project = GITHUB_PATH
|
||||||
|
edit_on_github_branch = code_branch
|
||||||
|
edit_on_github_src_path = "docs/source/"
|
||||||
|
|
||||||
|
|
||||||
|
def linkcode_resolve(domain, info):
|
||||||
|
"""Determine the URL corresponding to Python object."""
|
||||||
|
if domain != "py":
|
||||||
|
return None
|
||||||
|
modname = info["module"]
|
||||||
|
fullname = info["fullname"]
|
||||||
|
submod = sys.modules.get(modname)
|
||||||
|
if submod is None:
|
||||||
|
return None
|
||||||
|
obj = submod
|
||||||
|
for part in fullname.split("."):
|
||||||
|
try:
|
||||||
|
obj = getattr(obj, part)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
fn = inspect.getsourcefile(obj)
|
||||||
|
except:
|
||||||
|
fn = None
|
||||||
|
if not fn:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
source, lineno = inspect.findsource(obj)
|
||||||
|
except:
|
||||||
|
lineno = None
|
||||||
|
if lineno:
|
||||||
|
linespec = "#L%d" % (lineno + 1)
|
||||||
|
else:
|
||||||
|
linespec = ""
|
||||||
|
index = fn.find("/homeassistant/")
|
||||||
|
if index == -1:
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
fn = fn[index:]
|
||||||
|
|
||||||
|
return f"{GITHUB_URL}/blob/{code_branch}/{fn}{linespec}"
|
||||||
|
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#
|
||||||
|
# today = ''
|
||||||
|
#
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#
|
||||||
|
# today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
|
# documents.
|
||||||
|
#
|
||||||
|
# default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#
|
||||||
|
# add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#
|
||||||
|
# add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#
|
||||||
|
# show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
# modindex_common_prefix = []
|
||||||
|
|
||||||
|
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||||
|
# keep_warnings = False
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = "alabaster"
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
html_theme_options = {
|
||||||
|
"logo": "logo.png",
|
||||||
|
"logo_name": PROJECT_NAME,
|
||||||
|
"description": PROJECT_LONG_DESCRIPTION,
|
||||||
|
"github_user": PROJECT_GITHUB_USERNAME,
|
||||||
|
"github_repo": PROJECT_GITHUB_REPOSITORY,
|
||||||
|
"github_type": "star",
|
||||||
|
"github_banner": True,
|
||||||
|
"touch_icon": "logo-apple.png",
|
||||||
|
# 'fixed_sidebar': True, # Re-enable when we have more content
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
# html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents.
|
||||||
|
# "<project> v<release> documentation" by default.
|
||||||
|
#
|
||||||
|
# html_title = 'Home-Assistant v0.27.0'
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#
|
||||||
|
# html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#
|
||||||
|
# html_logo = '_static/logo.png'
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to use as a favicon of
|
||||||
|
# the docs.
|
||||||
|
# This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#
|
||||||
|
html_favicon = "_static/favicon.ico"
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
# Add any extra paths that contain custom files (such as robots.txt or
|
||||||
|
# .htaccess) here, relative to this directory. These files are copied
|
||||||
|
# directly to the root of the documentation.
|
||||||
|
#
|
||||||
|
# html_extra_path = []
|
||||||
|
|
||||||
|
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||||
|
# bottom, using the given strftime format.
|
||||||
|
# The empty string is equivalent to '%b %d, %Y'.
|
||||||
|
#
|
||||||
|
html_last_updated_fmt = "%b %d, %Y"
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#
|
||||||
|
html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#
|
||||||
|
html_sidebars = {
|
||||||
|
"**": [
|
||||||
|
"about.html",
|
||||||
|
"links.html",
|
||||||
|
"searchbox.html",
|
||||||
|
"sourcelink.html",
|
||||||
|
"navigation.html",
|
||||||
|
"relations.html",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#
|
||||||
|
# html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
# html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#
|
||||||
|
# html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#
|
||||||
|
# html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#
|
||||||
|
# html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#
|
||||||
|
# html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#
|
||||||
|
# html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#
|
||||||
|
# html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
# html_file_suffix = None
|
||||||
|
|
||||||
|
# Language to be used for generating the HTML full-text search index.
|
||||||
|
# Sphinx supports the following languages:
|
||||||
|
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||||
|
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
|
||||||
|
#
|
||||||
|
# html_search_language = 'en'
|
||||||
|
|
||||||
|
# A dictionary with options for the search language support, empty by default.
|
||||||
|
# 'ja' uses this config value.
|
||||||
|
# 'zh' user can custom change `jieba` dictionary path.
|
||||||
|
#
|
||||||
|
# html_search_options = {'type': 'default'}
|
||||||
|
|
||||||
|
# The name of a javascript file (relative to the configuration directory) that
|
||||||
|
# implements a search results scorer. If empty, the default will be used.
|
||||||
|
#
|
||||||
|
# html_search_scorer = 'scorer.js'
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = "Home-Assistantdoc"
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(
|
||||||
|
master_doc,
|
||||||
|
"home-assistant.tex",
|
||||||
|
"Home Assistant Documentation",
|
||||||
|
"Home Assistant Team",
|
||||||
|
"manual",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#
|
||||||
|
# latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#
|
||||||
|
# latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#
|
||||||
|
# latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#
|
||||||
|
# latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#
|
||||||
|
# latex_appendices = []
|
||||||
|
|
||||||
|
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
||||||
|
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
||||||
|
# packages.
|
||||||
|
#
|
||||||
|
# latex_keep_old_macro_names = True
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
# latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, "home-assistant", "Home Assistant Documentation", [author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#
|
||||||
|
# man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(
|
||||||
|
master_doc,
|
||||||
|
"Home-Assistant",
|
||||||
|
"Home Assistant Documentation",
|
||||||
|
author,
|
||||||
|
"Home Assistant",
|
||||||
|
"Open-source home automation platform.",
|
||||||
|
"Miscellaneous",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#
|
||||||
|
# texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
# texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#
|
||||||
|
# texinfo_show_urls = 'footnote'
|
||||||
|
|
||||||
|
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||||
|
#
|
||||||
|
# texinfo_no_detailmenu = False
|
22
docs/source/index.rst
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
================================
|
||||||
|
Home Assistant API Documentation
|
||||||
|
================================
|
||||||
|
|
||||||
|
Public API documentation for `Home Assistant developers`_.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
api/*
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. _Home Assistant developers: https://developers.home-assistant.io/
|
|
@ -1,15 +1,12 @@
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from contextlib import suppress
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .backup_restore import restore_backup
|
|
||||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||||
|
|
||||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||||
|
@ -18,10 +15,7 @@ FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||||
def validate_os() -> None:
|
def validate_os() -> None:
|
||||||
"""Validate that Home Assistant is running in a supported operating system."""
|
"""Validate that Home Assistant is running in a supported operating system."""
|
||||||
if not sys.platform.startswith(("darwin", "linux")):
|
if not sys.platform.startswith(("darwin", "linux")):
|
||||||
print(
|
print("Home Assistant only supports Linux, OSX and Windows using WSL")
|
||||||
"Home Assistant only supports Linux, OSX and Windows using WSL",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,15 +24,14 @@ def validate_python() -> None:
|
||||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||||
print(
|
print(
|
||||||
"Home Assistant requires at least Python "
|
"Home Assistant requires at least Python "
|
||||||
f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}",
|
f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}"
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_path(config_dir: str) -> None:
|
def ensure_config_path(config_dir: str) -> None:
|
||||||
"""Validate the configuration directory."""
|
"""Validate the configuration directory."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from . import config as config_util
|
from . import config as config_util
|
||||||
|
|
||||||
lib_dir = os.path.join(config_dir, "deps")
|
lib_dir = os.path.join(config_dir, "deps")
|
||||||
|
@ -46,23 +39,18 @@ def ensure_config_path(config_dir: str) -> None:
|
||||||
# Test if configuration directory exists
|
# Test if configuration directory exists
|
||||||
if not os.path.isdir(config_dir):
|
if not os.path.isdir(config_dir):
|
||||||
if config_dir != config_util.get_default_config_dir():
|
if config_dir != config_util.get_default_config_dir():
|
||||||
if os.path.exists(config_dir):
|
|
||||||
reason = "is not a directory"
|
|
||||||
else:
|
|
||||||
reason = "does not exist"
|
|
||||||
print(
|
print(
|
||||||
f"Fatal Error: Specified configuration directory {config_dir} {reason}",
|
f"Fatal Error: Specified configuration directory {config_dir} "
|
||||||
file=sys.stderr,
|
"does not exist"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(config_dir)
|
os.mkdir(config_dir)
|
||||||
except OSError as ex:
|
except OSError:
|
||||||
print(
|
print(
|
||||||
"Fatal Error: Unable to create default configuration "
|
"Fatal Error: Unable to create default configuration "
|
||||||
f"directory {config_dir}: {ex}",
|
f"directory {config_dir}"
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@ -70,17 +58,14 @@ def ensure_config_path(config_dir: str) -> None:
|
||||||
if not os.path.isdir(lib_dir):
|
if not os.path.isdir(lib_dir):
|
||||||
try:
|
try:
|
||||||
os.mkdir(lib_dir)
|
os.mkdir(lib_dir)
|
||||||
except OSError as ex:
|
except OSError:
|
||||||
print(
|
print(f"Fatal Error: Unable to create library directory {lib_dir}")
|
||||||
f"Fatal Error: Unable to create library directory {lib_dir}: {ex}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_arguments() -> argparse.Namespace:
|
def get_arguments() -> argparse.Namespace:
|
||||||
"""Get parsed passed in arguments."""
|
"""Get parsed passed in arguments."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from . import config as config_util
|
from . import config as config_util
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
@ -96,9 +81,7 @@ def get_arguments() -> argparse.Namespace:
|
||||||
help="Directory that contains the Home Assistant configuration",
|
help="Directory that contains the Home Assistant configuration",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--recovery-mode",
|
"--safe-mode", action="store_true", help="Start Home Assistant in safe mode"
|
||||||
action="store_true",
|
|
||||||
help="Start Home Assistant in recovery mode",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug", action="store_true", help="Start Home Assistant in debug mode"
|
"--debug", action="store_true", help="Start Home Assistant in debug mode"
|
||||||
|
@ -106,21 +89,11 @@ def get_arguments() -> argparse.Namespace:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--open-ui", action="store_true", help="Open the webinterface in a browser"
|
"--open-ui", action="store_true", help="Open the webinterface in a browser"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
skip_pip_group = parser.add_mutually_exclusive_group()
|
|
||||||
skip_pip_group.add_argument(
|
|
||||||
"--skip-pip",
|
"--skip-pip",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Skips pip install of required packages on startup",
|
help="Skips pip install of required packages on startup",
|
||||||
)
|
)
|
||||||
skip_pip_group.add_argument(
|
|
||||||
"--skip-pip-packages",
|
|
||||||
metavar="package_names",
|
|
||||||
type=lambda arg: arg.split(","),
|
|
||||||
default=[],
|
|
||||||
help="Skip pip install of specific packages on startup",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
||||||
)
|
)
|
||||||
|
@ -148,7 +121,19 @@ def get_arguments() -> argparse.Namespace:
|
||||||
help="Skips validation of operating system",
|
help="Skips validation of operating system",
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
def cmdline() -> list[str]:
|
||||||
|
"""Collect path and arguments to re-execute the current hass instance."""
|
||||||
|
if os.path.basename(sys.argv[0]) == "__main__.py":
|
||||||
|
modulepath = os.path.dirname(sys.argv[0])
|
||||||
|
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
|
||||||
|
return [sys.executable, "-m", "homeassistant"] + list(sys.argv[1:])
|
||||||
|
|
||||||
|
return sys.argv
|
||||||
|
|
||||||
|
|
||||||
def check_threads() -> None:
|
def check_threads() -> None:
|
||||||
|
@ -177,21 +162,16 @@ def main() -> int:
|
||||||
validate_os()
|
validate_os()
|
||||||
|
|
||||||
if args.script is not None:
|
if args.script is not None:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from . import scripts
|
from . import scripts
|
||||||
|
|
||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||||
if restore_backup(config_dir):
|
|
||||||
return RESTART_EXIT_CODE
|
|
||||||
|
|
||||||
ensure_config_path(config_dir)
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from . import config, runner
|
from . import runner
|
||||||
|
|
||||||
safe_mode = config.safe_mode_enabled(config_dir)
|
|
||||||
|
|
||||||
runtime_conf = runner.RuntimeConfig(
|
runtime_conf = runner.RuntimeConfig(
|
||||||
config_dir=config_dir,
|
config_dir=config_dir,
|
||||||
|
@ -200,11 +180,9 @@ def main() -> int:
|
||||||
log_file=args.log_file,
|
log_file=args.log_file,
|
||||||
log_no_color=args.log_no_color,
|
log_no_color=args.log_no_color,
|
||||||
skip_pip=args.skip_pip,
|
skip_pip=args.skip_pip,
|
||||||
skip_pip_packages=args.skip_pip_packages,
|
safe_mode=args.safe_mode,
|
||||||
recovery_mode=args.recovery_mode,
|
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
open_ui=args.open_ui,
|
open_ui=args.open_ui,
|
||||||
safe_mode=safe_mode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
||||||
|
@ -213,8 +191,6 @@ def main() -> int:
|
||||||
exit_code = runner.run(runtime_conf)
|
exit_code = runner.run(runtime_conf)
|
||||||
faulthandler.disable()
|
faulthandler.disable()
|
||||||
|
|
||||||
# It's possible for the fault file to disappear, so suppress obvious errors
|
|
||||||
with suppress(FileNotFoundError):
|
|
||||||
if os.path.getsize(fault_file_name) == 0:
|
if os.path.getsize(fault_file_name) == 0:
|
||||||
os.remove(fault_file_name)
|
os.remove(fault_file_name)
|
||||||
|
|
||||||
|
|
|
@ -1,42 +1,30 @@
|
||||||
"""Provide an authentication layer for Home Assistant."""
|
"""Provide an authentication layer for Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
from typing import Any, Optional, cast
|
||||||
import time
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from homeassistant.core import (
|
from homeassistant import data_entry_flow
|
||||||
CALLBACK_TYPE,
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
HassJob,
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
HassJobType,
|
|
||||||
HomeAssistant,
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType
|
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import auth_store, jwt_wrapper, models
|
from . import auth_store, models
|
||||||
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
|
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN
|
||||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||||
from .models import AuthFlowContext, AuthFlowResult
|
|
||||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||||
from .providers.homeassistant import HassAuthProvider
|
|
||||||
|
|
||||||
EVENT_USER_ADDED = "user_added"
|
EVENT_USER_ADDED = "user_added"
|
||||||
EVENT_USER_UPDATED = "user_updated"
|
|
||||||
EVENT_USER_REMOVED = "user_removed"
|
EVENT_USER_REMOVED = "user_removed"
|
||||||
|
|
||||||
type _MfaModuleDict = dict[str, MultiFactorAuthModule]
|
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||||
type _ProviderKey = tuple[str, str | None]
|
_ProviderKey = tuple[str, Optional[str]]
|
||||||
type _ProviderDict = dict[_ProviderKey, AuthProvider]
|
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(Exception):
|
class InvalidAuthError(Exception):
|
||||||
|
@ -54,11 +42,10 @@ async def auth_manager_from_config(
|
||||||
) -> AuthManager:
|
) -> AuthManager:
|
||||||
"""Initialize an auth manager from config.
|
"""Initialize an auth manager from config.
|
||||||
|
|
||||||
CORE_CONFIG_SCHEMA will make sure no duplicated auth providers or
|
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||||
mfa modules exist in configs.
|
mfa modules exist in configs.
|
||||||
"""
|
"""
|
||||||
store = auth_store.AuthStore(hass)
|
store = auth_store.AuthStore(hass)
|
||||||
await store.async_load()
|
|
||||||
if provider_configs:
|
if provider_configs:
|
||||||
providers = await asyncio.gather(
|
providers = await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
|
@ -74,13 +61,6 @@ async def auth_manager_from_config(
|
||||||
key = (provider.type, provider.id)
|
key = (provider.type, provider.id)
|
||||||
provider_hash[key] = provider
|
provider_hash[key] = provider
|
||||||
|
|
||||||
if isinstance(provider, HassAuthProvider):
|
|
||||||
# Can be removed in 2026.7 with the legacy mode of homeassistant auth provider
|
|
||||||
# We need to initialize the provider to create the repair if needed as otherwise
|
|
||||||
# the provider will be initialized on first use, which could be rare as users
|
|
||||||
# don't frequently change auth settings
|
|
||||||
await provider.async_initialize()
|
|
||||||
|
|
||||||
if module_configs:
|
if module_configs:
|
||||||
modules = await asyncio.gather(
|
modules = await asyncio.gather(
|
||||||
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
||||||
|
@ -93,17 +73,12 @@ async def auth_manager_from_config(
|
||||||
module_hash[module.id] = module
|
module_hash[module.id] = module
|
||||||
|
|
||||||
manager = AuthManager(hass, store, provider_hash, module_hash)
|
manager = AuthManager(hass, store, provider_hash, module_hash)
|
||||||
await manager.async_setup()
|
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
class AuthManagerFlowManager(
|
class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||||
FlowManager[AuthFlowContext, AuthFlowResult, tuple[str, str]]
|
|
||||||
):
|
|
||||||
"""Manage authentication flows."""
|
"""Manage authentication flows."""
|
||||||
|
|
||||||
_flow_result = AuthFlowResult
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None:
|
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None:
|
||||||
"""Init auth manager flows."""
|
"""Init auth manager flows."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
|
@ -111,11 +86,11 @@ class AuthManagerFlowManager(
|
||||||
|
|
||||||
async def async_create_flow(
|
async def async_create_flow(
|
||||||
self,
|
self,
|
||||||
handler_key: tuple[str, str],
|
handler_key: Any,
|
||||||
*,
|
*,
|
||||||
context: AuthFlowContext | None = None,
|
context: dict[str, Any] | None = None,
|
||||||
data: dict[str, Any] | None = None,
|
data: dict[str, Any] | None = None,
|
||||||
) -> LoginFlow:
|
) -> data_entry_flow.FlowHandler:
|
||||||
"""Create a login flow."""
|
"""Create a login flow."""
|
||||||
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
||||||
if not auth_provider:
|
if not auth_provider:
|
||||||
|
@ -123,18 +98,12 @@ class AuthManagerFlowManager(
|
||||||
return await auth_provider.async_login_flow(context)
|
return await auth_provider.async_login_flow(context)
|
||||||
|
|
||||||
async def async_finish_flow(
|
async def async_finish_flow(
|
||||||
self,
|
self, flow: data_entry_flow.FlowHandler, result: FlowResult
|
||||||
flow: FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
|
) -> FlowResult:
|
||||||
result: AuthFlowResult,
|
"""Return a user as result of login flow."""
|
||||||
) -> AuthFlowResult:
|
|
||||||
"""Return a user as result of login flow.
|
|
||||||
|
|
||||||
This method is called when a flow step returns FlowResultType.ABORT or
|
|
||||||
FlowResultType.CREATE_ENTRY.
|
|
||||||
"""
|
|
||||||
flow = cast(LoginFlow, flow)
|
flow = cast(LoginFlow, flow)
|
||||||
|
|
||||||
if result["type"] != FlowResultType.CREATE_ENTRY:
|
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# we got final result
|
# we got final result
|
||||||
|
@ -187,21 +156,7 @@ class AuthManager:
|
||||||
self._providers = providers
|
self._providers = providers
|
||||||
self._mfa_modules = mfa_modules
|
self._mfa_modules = mfa_modules
|
||||||
self.login_flow = AuthManagerFlowManager(hass, self)
|
self.login_flow = AuthManagerFlowManager(hass, self)
|
||||||
self._revoke_callbacks: dict[str, set[CALLBACK_TYPE]] = {}
|
self._revoke_callbacks: dict[str, list[CALLBACK_TYPE]] = {}
|
||||||
self._expire_callback: CALLBACK_TYPE | None = None
|
|
||||||
self._remove_expired_job = HassJob(
|
|
||||||
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
|
||||||
"""Set up the auth manager."""
|
|
||||||
hass = self.hass
|
|
||||||
hass.async_add_shutdown_job(
|
|
||||||
HassJob(
|
|
||||||
self._async_cancel_expiration_schedule, job_type=HassJobType.Callback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._async_track_next_refresh_token_expiration()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auth_providers(self) -> list[AuthProvider]:
|
def auth_providers(self) -> list[AuthProvider]:
|
||||||
|
@ -324,8 +279,7 @@ class AuthManager:
|
||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
name=info.name,
|
name=info.name,
|
||||||
is_active=info.is_active,
|
is_active=info.is_active,
|
||||||
group_ids=[GROUP_ID_ADMIN if info.group is None else info.group],
|
group_ids=[GROUP_ID_ADMIN],
|
||||||
local_only=info.local_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
|
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
|
||||||
|
@ -367,15 +321,15 @@ class AuthManager:
|
||||||
local_only: bool | None = None,
|
local_only: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update a user."""
|
"""Update a user."""
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {}
|
||||||
attr_name: value
|
|
||||||
for attr_name, value in (
|
for attr_name, value in (
|
||||||
("name", name),
|
("name", name),
|
||||||
("group_ids", group_ids),
|
("group_ids", group_ids),
|
||||||
("local_only", local_only),
|
("local_only", local_only),
|
||||||
)
|
):
|
||||||
if value is not None
|
if value is not None:
|
||||||
}
|
kwargs[attr_name] = value
|
||||||
await self._store.async_update_user(user, **kwargs)
|
await self._store.async_update_user(user, **kwargs)
|
||||||
|
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
|
@ -384,15 +338,6 @@ class AuthManager:
|
||||||
else:
|
else:
|
||||||
await self.async_deactivate_user(user)
|
await self.async_deactivate_user(user)
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id})
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_user_credentials_data(
|
|
||||||
self, credentials: models.Credentials, data: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""Update credentials data."""
|
|
||||||
self._store.async_update_user_credentials_data(credentials, data=data)
|
|
||||||
|
|
||||||
async def async_activate_user(self, user: models.User) -> None:
|
async def async_activate_user(self, user: models.User) -> None:
|
||||||
"""Activate a user."""
|
"""Activate a user."""
|
||||||
await self._store.async_activate_user(user)
|
await self._store.async_activate_user(user)
|
||||||
|
@ -408,7 +353,8 @@ class AuthManager:
|
||||||
provider = self._async_get_auth_provider(credentials)
|
provider = self._async_get_auth_provider(credentials)
|
||||||
|
|
||||||
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
|
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
|
||||||
await provider.async_will_remove_credentials(credentials)
|
# https://github.com/python/mypy/issues/1424
|
||||||
|
await provider.async_will_remove_credentials(credentials) # type: ignore
|
||||||
|
|
||||||
await self._store.async_remove_credentials(credentials)
|
await self._store.async_remove_credentials(credentials)
|
||||||
|
|
||||||
|
@ -474,11 +420,6 @@ class AuthManager:
|
||||||
else:
|
else:
|
||||||
token_type = models.TOKEN_TYPE_NORMAL
|
token_type = models.TOKEN_TYPE_NORMAL
|
||||||
|
|
||||||
if token_type is models.TOKEN_TYPE_NORMAL:
|
|
||||||
expire_at = time.time() + REFRESH_TOKEN_EXPIRATION
|
|
||||||
else:
|
|
||||||
expire_at = None
|
|
||||||
|
|
||||||
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"System generated users can only have system type refresh tokens"
|
"System generated users can only have system type refresh tokens"
|
||||||
|
@ -510,88 +451,48 @@ class AuthManager:
|
||||||
client_icon,
|
client_icon,
|
||||||
token_type,
|
token_type,
|
||||||
access_token_expiration,
|
access_token_expiration,
|
||||||
expire_at,
|
|
||||||
credential,
|
credential,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
async def async_get_refresh_token(
|
||||||
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
self, token_id: str
|
||||||
|
) -> models.RefreshToken | None:
|
||||||
"""Get refresh token by id."""
|
"""Get refresh token by id."""
|
||||||
return self._store.async_get_refresh_token(token_id)
|
return await self._store.async_get_refresh_token(token_id)
|
||||||
|
|
||||||
@callback
|
async def async_get_refresh_token_by_token(
|
||||||
def async_get_refresh_token_by_token(
|
|
||||||
self, token: str
|
self, token: str
|
||||||
) -> models.RefreshToken | None:
|
) -> models.RefreshToken | None:
|
||||||
"""Get refresh token by token."""
|
"""Get refresh token by token."""
|
||||||
return self._store.async_get_refresh_token_by_token(token)
|
return await self._store.async_get_refresh_token_by_token(token)
|
||||||
|
|
||||||
@callback
|
async def async_remove_refresh_token(
|
||||||
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
self, refresh_token: models.RefreshToken
|
||||||
|
) -> None:
|
||||||
"""Delete a refresh token."""
|
"""Delete a refresh token."""
|
||||||
self._store.async_remove_refresh_token(refresh_token)
|
await self._store.async_remove_refresh_token(refresh_token)
|
||||||
|
|
||||||
callbacks = self._revoke_callbacks.pop(refresh_token.id, ())
|
callbacks = self._revoke_callbacks.pop(refresh_token.id, [])
|
||||||
for revoke_callback in callbacks:
|
for revoke_callback in callbacks:
|
||||||
revoke_callback()
|
revoke_callback()
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_set_expiry(
|
|
||||||
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
|
||||||
) -> None:
|
|
||||||
"""Enable or disable expiry of a refresh token."""
|
|
||||||
self._store.async_set_expiry(refresh_token, enable_expiry=enable_expiry)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
|
|
||||||
"""Remove expired refresh tokens."""
|
|
||||||
now = time.time()
|
|
||||||
for token in self._store.async_get_refresh_tokens():
|
|
||||||
if (expire_at := token.expire_at) is not None and expire_at <= now:
|
|
||||||
self.async_remove_refresh_token(token)
|
|
||||||
self._async_track_next_refresh_token_expiration()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_track_next_refresh_token_expiration(self) -> None:
|
|
||||||
"""Initialise all token expiration scheduled tasks."""
|
|
||||||
next_expiration = time.time() + REFRESH_TOKEN_EXPIRATION
|
|
||||||
for token in self._store.async_get_refresh_tokens():
|
|
||||||
if (
|
|
||||||
expire_at := token.expire_at
|
|
||||||
) is not None and expire_at < next_expiration:
|
|
||||||
next_expiration = expire_at
|
|
||||||
|
|
||||||
self._expire_callback = async_track_point_in_utc_time(
|
|
||||||
self.hass,
|
|
||||||
self._remove_expired_job,
|
|
||||||
dt_util.utc_from_timestamp(next_expiration),
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_cancel_expiration_schedule(self) -> None:
|
|
||||||
"""Cancel tracking of expired refresh tokens."""
|
|
||||||
if self._expire_callback:
|
|
||||||
self._expire_callback()
|
|
||||||
self._expire_callback = None
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unregister(
|
|
||||||
self, callbacks: set[CALLBACK_TYPE], callback_: CALLBACK_TYPE
|
|
||||||
) -> None:
|
|
||||||
"""Unregister a callback."""
|
|
||||||
callbacks.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_revoke_token_callback(
|
def async_register_revoke_token_callback(
|
||||||
self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE
|
self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register a callback to be called when the refresh token id is revoked."""
|
"""Register a callback to be called when the refresh token id is revoked."""
|
||||||
if refresh_token_id not in self._revoke_callbacks:
|
if refresh_token_id not in self._revoke_callbacks:
|
||||||
self._revoke_callbacks[refresh_token_id] = set()
|
self._revoke_callbacks[refresh_token_id] = []
|
||||||
|
|
||||||
callbacks = self._revoke_callbacks[refresh_token_id]
|
callbacks = self._revoke_callbacks[refresh_token_id]
|
||||||
callbacks.add(revoke_callback)
|
callbacks.append(revoke_callback)
|
||||||
return partial(self._async_unregister, callbacks, revoke_callback)
|
|
||||||
|
@callback
|
||||||
|
def unregister() -> None:
|
||||||
|
if revoke_callback in callbacks:
|
||||||
|
callbacks.remove(revoke_callback)
|
||||||
|
|
||||||
|
return unregister
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_create_access_token(
|
def async_create_access_token(
|
||||||
|
@ -602,13 +503,12 @@ class AuthManager:
|
||||||
|
|
||||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||||
|
|
||||||
now = int(time.time())
|
now = dt_util.utcnow()
|
||||||
expire_seconds = int(refresh_token.access_token_expiration.total_seconds())
|
|
||||||
return jwt.encode(
|
return jwt.encode(
|
||||||
{
|
{
|
||||||
"iss": refresh_token.id,
|
"iss": refresh_token.id,
|
||||||
"iat": now,
|
"iat": now,
|
||||||
"exp": now + expire_seconds,
|
"exp": now + refresh_token.access_token_expiration,
|
||||||
},
|
},
|
||||||
refresh_token.jwt_key,
|
refresh_token.jwt_key,
|
||||||
algorithm="HS256",
|
algorithm="HS256",
|
||||||
|
@ -632,8 +532,7 @@ class AuthManager:
|
||||||
)
|
)
|
||||||
if provider is None:
|
if provider is None:
|
||||||
raise InvalidProvider(
|
raise InvalidProvider(
|
||||||
f"Auth provider {refresh_token.credential.auth_provider_type},"
|
f"Auth provider {refresh_token.credential.auth_provider_type}, {refresh_token.credential.auth_provider_id} not available"
|
||||||
f" {refresh_token.credential.auth_provider_id} not available"
|
|
||||||
)
|
)
|
||||||
return provider
|
return provider
|
||||||
|
|
||||||
|
@ -648,15 +547,18 @@ class AuthManager:
|
||||||
if provider := self._async_resolve_provider(refresh_token):
|
if provider := self._async_resolve_provider(refresh_token):
|
||||||
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
||||||
|
|
||||||
@callback
|
async def async_validate_access_token(
|
||||||
def async_validate_access_token(self, token: str) -> models.RefreshToken | None:
|
self, token: str
|
||||||
|
) -> models.RefreshToken | None:
|
||||||
"""Return refresh token if an access token is valid."""
|
"""Return refresh token if an access token is valid."""
|
||||||
try:
|
try:
|
||||||
unverif_claims = jwt_wrapper.unverified_hs256_token_decode(token)
|
unverif_claims = jwt.decode(
|
||||||
|
token, algorithms=["HS256"], options={"verify_signature": False}
|
||||||
|
)
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
refresh_token = self.async_get_refresh_token(
|
refresh_token = await self.async_get_refresh_token(
|
||||||
cast(str, unverif_claims.get("iss"))
|
cast(str, unverif_claims.get("iss"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -668,9 +570,7 @@ class AuthManager:
|
||||||
issuer = refresh_token.id
|
issuer = refresh_token.id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jwt_wrapper.verify_and_decode(
|
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
|
||||||
token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"]
|
|
||||||
)
|
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
"""Storage for auth models."""
|
"""Storage for auth models."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections import OrderedDict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import hmac
|
import hmac
|
||||||
import itertools
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -19,7 +17,6 @@ from .const import (
|
||||||
GROUP_ID_ADMIN,
|
GROUP_ID_ADMIN,
|
||||||
GROUP_ID_READ_ONLY,
|
GROUP_ID_READ_ONLY,
|
||||||
GROUP_ID_USER,
|
GROUP_ID_USER,
|
||||||
REFRESH_TOKEN_EXPIRATION,
|
|
||||||
)
|
)
|
||||||
from .permissions import system_policies
|
from .permissions import system_policies
|
||||||
from .permissions.models import PermissionLookup
|
from .permissions.models import PermissionLookup
|
||||||
|
@ -31,17 +28,6 @@ GROUP_NAME_ADMIN = "Administrators"
|
||||||
GROUP_NAME_USER = "Users"
|
GROUP_NAME_USER = "Users"
|
||||||
GROUP_NAME_READ_ONLY = "Read Only"
|
GROUP_NAME_READ_ONLY = "Read Only"
|
||||||
|
|
||||||
# We always save the auth store after we load it since
|
|
||||||
# we may migrate data and do not want to have to do it again
|
|
||||||
# but we don't want to do it during startup so we schedule
|
|
||||||
# the first save 5 minutes out knowing something else may
|
|
||||||
# want to save the auth store before then, and since Storage
|
|
||||||
# will honor the lower of the two delays, it will save it
|
|
||||||
# faster if something else saves it.
|
|
||||||
INITIAL_LOAD_SAVE_DELAY = 300
|
|
||||||
|
|
||||||
DEFAULT_SAVE_DELAY = 1
|
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
"""Stores authentication info.
|
"""Stores authentication info.
|
||||||
|
@ -55,29 +41,44 @@ class AuthStore:
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the auth store."""
|
"""Initialize the auth store."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._loaded = False
|
self._users: dict[str, models.User] | None = None
|
||||||
self._users: dict[str, models.User] = None # type: ignore[assignment]
|
self._groups: dict[str, models.Group] | None = None
|
||||||
self._groups: dict[str, models.Group] = None # type: ignore[assignment]
|
self._perm_lookup: PermissionLookup | None = None
|
||||||
self._perm_lookup: PermissionLookup = None # type: ignore[assignment]
|
self._store = hass.helpers.storage.Store(
|
||||||
self._store = Store[dict[str, list[dict[str, Any]]]](
|
STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
|
||||||
)
|
)
|
||||||
self._token_id_to_user_id: dict[str, str] = {}
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
async def async_get_groups(self) -> list[models.Group]:
|
async def async_get_groups(self) -> list[models.Group]:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
|
if self._groups is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
return list(self._groups.values())
|
return list(self._groups.values())
|
||||||
|
|
||||||
async def async_get_group(self, group_id: str) -> models.Group | None:
|
async def async_get_group(self, group_id: str) -> models.Group | None:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
|
if self._groups is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
return self._groups.get(group_id)
|
return self._groups.get(group_id)
|
||||||
|
|
||||||
async def async_get_users(self) -> list[models.User]:
|
async def async_get_users(self) -> list[models.User]:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
|
if self._users is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._users is not None
|
||||||
|
|
||||||
return list(self._users.values())
|
return list(self._users.values())
|
||||||
|
|
||||||
async def async_get_user(self, user_id: str) -> models.User | None:
|
async def async_get_user(self, user_id: str) -> models.User | None:
|
||||||
"""Retrieve a user by id."""
|
"""Retrieve a user by id."""
|
||||||
|
if self._users is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._users is not None
|
||||||
|
|
||||||
return self._users.get(user_id)
|
return self._users.get(user_id)
|
||||||
|
|
||||||
async def async_create_user(
|
async def async_create_user(
|
||||||
|
@ -91,6 +92,12 @@ class AuthStore:
|
||||||
local_only: bool | None = None,
|
local_only: bool | None = None,
|
||||||
) -> models.User:
|
) -> models.User:
|
||||||
"""Create a new user."""
|
"""Create a new user."""
|
||||||
|
if self._users is None:
|
||||||
|
await self._async_load()
|
||||||
|
|
||||||
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
groups = []
|
groups = []
|
||||||
for group_id in group_ids or []:
|
for group_id in group_ids or []:
|
||||||
if (group := self._groups.get(group_id)) is None:
|
if (group := self._groups.get(group_id)) is None:
|
||||||
|
@ -105,18 +112,14 @@ class AuthStore:
|
||||||
"perm_lookup": self._perm_lookup,
|
"perm_lookup": self._perm_lookup,
|
||||||
}
|
}
|
||||||
|
|
||||||
kwargs.update(
|
|
||||||
{
|
|
||||||
attr_name: value
|
|
||||||
for attr_name, value in (
|
for attr_name, value in (
|
||||||
("is_owner", is_owner),
|
("is_owner", is_owner),
|
||||||
("is_active", is_active),
|
("is_active", is_active),
|
||||||
("local_only", local_only),
|
("local_only", local_only),
|
||||||
("system_generated", system_generated),
|
("system_generated", system_generated),
|
||||||
)
|
):
|
||||||
if value is not None
|
if value is not None:
|
||||||
}
|
kwargs[attr_name] = value
|
||||||
)
|
|
||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
|
@ -140,10 +143,11 @@ class AuthStore:
|
||||||
|
|
||||||
async def async_remove_user(self, user: models.User) -> None:
|
async def async_remove_user(self, user: models.User) -> None:
|
||||||
"""Remove a user."""
|
"""Remove a user."""
|
||||||
user = self._users.pop(user.id)
|
if self._users is None:
|
||||||
for refresh_token_id in user.refresh_tokens:
|
await self._async_load()
|
||||||
del self._token_id_to_user_id[refresh_token_id]
|
assert self._users is not None
|
||||||
user.refresh_tokens.clear()
|
|
||||||
|
self._users.pop(user.id)
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_update_user(
|
async def async_update_user(
|
||||||
|
@ -155,6 +159,8 @@ class AuthStore:
|
||||||
local_only: bool | None = None,
|
local_only: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update a user."""
|
"""Update a user."""
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
if group_ids is not None:
|
if group_ids is not None:
|
||||||
groups = []
|
groups = []
|
||||||
for grid in group_ids:
|
for grid in group_ids:
|
||||||
|
@ -163,6 +169,7 @@ class AuthStore:
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
user.groups = groups
|
user.groups = groups
|
||||||
|
user.invalidate_permission_cache()
|
||||||
|
|
||||||
for attr_name, value in (
|
for attr_name, value in (
|
||||||
("name", name),
|
("name", name),
|
||||||
|
@ -186,6 +193,10 @@ class AuthStore:
|
||||||
|
|
||||||
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||||
"""Remove credentials."""
|
"""Remove credentials."""
|
||||||
|
if self._users is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._users is not None
|
||||||
|
|
||||||
for user in self._users.values():
|
for user in self._users.values():
|
||||||
found = None
|
found = None
|
||||||
|
|
||||||
|
@ -208,7 +219,6 @@ class AuthStore:
|
||||||
client_icon: str | None = None,
|
client_icon: str | None = None,
|
||||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||||
expire_at: float | None = None,
|
|
||||||
credential: models.Credentials | None = None,
|
credential: models.Credentials | None = None,
|
||||||
) -> models.RefreshToken:
|
) -> models.RefreshToken:
|
||||||
"""Create a new token for a user."""
|
"""Create a new token for a user."""
|
||||||
|
@ -217,7 +227,6 @@ class AuthStore:
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
"token_type": token_type,
|
"token_type": token_type,
|
||||||
"access_token_expiration": access_token_expiration,
|
"access_token_expiration": access_token_expiration,
|
||||||
"expire_at": expire_at,
|
|
||||||
"credential": credential,
|
"credential": credential,
|
||||||
}
|
}
|
||||||
if client_name:
|
if client_name:
|
||||||
|
@ -226,34 +235,47 @@ class AuthStore:
|
||||||
kwargs["client_icon"] = client_icon
|
kwargs["client_icon"] = client_icon
|
||||||
|
|
||||||
refresh_token = models.RefreshToken(**kwargs)
|
refresh_token = models.RefreshToken(**kwargs)
|
||||||
token_id = refresh_token.id
|
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||||
user.refresh_tokens[token_id] = refresh_token
|
|
||||||
self._token_id_to_user_id[token_id] = user.id
|
|
||||||
|
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
return refresh_token
|
return refresh_token
|
||||||
|
|
||||||
@callback
|
async def async_remove_refresh_token(
|
||||||
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
self, refresh_token: models.RefreshToken
|
||||||
|
) -> None:
|
||||||
"""Remove a refresh token."""
|
"""Remove a refresh token."""
|
||||||
refresh_token_id = refresh_token.id
|
if self._users is None:
|
||||||
if user_id := self._token_id_to_user_id.get(refresh_token_id):
|
await self._async_load()
|
||||||
del self._users[user_id].refresh_tokens[refresh_token_id]
|
assert self._users is not None
|
||||||
del self._token_id_to_user_id[refresh_token_id]
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
@callback
|
for user in self._users.values():
|
||||||
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
if user.refresh_tokens.pop(refresh_token.id, None):
|
||||||
|
self._async_schedule_save()
|
||||||
|
break
|
||||||
|
|
||||||
|
async def async_get_refresh_token(
|
||||||
|
self, token_id: str
|
||||||
|
) -> models.RefreshToken | None:
|
||||||
"""Get refresh token by id."""
|
"""Get refresh token by id."""
|
||||||
if user_id := self._token_id_to_user_id.get(token_id):
|
if self._users is None:
|
||||||
return self._users[user_id].refresh_tokens.get(token_id)
|
await self._async_load()
|
||||||
|
assert self._users is not None
|
||||||
|
|
||||||
|
for user in self._users.values():
|
||||||
|
refresh_token = user.refresh_tokens.get(token_id)
|
||||||
|
if refresh_token is not None:
|
||||||
|
return refresh_token
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@callback
|
async def async_get_refresh_token_by_token(
|
||||||
def async_get_refresh_token_by_token(
|
|
||||||
self, token: str
|
self, token: str
|
||||||
) -> models.RefreshToken | None:
|
) -> models.RefreshToken | None:
|
||||||
"""Get refresh token by token."""
|
"""Get refresh token by token."""
|
||||||
|
if self._users is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._users is not None
|
||||||
|
|
||||||
found = None
|
found = None
|
||||||
|
|
||||||
for user in self._users.values():
|
for user in self._users.values():
|
||||||
|
@ -263,15 +285,6 @@ class AuthStore:
|
||||||
|
|
||||||
return found
|
return found
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_get_refresh_tokens(self) -> list[models.RefreshToken]:
|
|
||||||
"""Get all refresh tokens."""
|
|
||||||
return list(
|
|
||||||
itertools.chain.from_iterable(
|
|
||||||
user.refresh_tokens.values() for user in self._users.values()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_log_refresh_token_usage(
|
def async_log_refresh_token_usage(
|
||||||
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
||||||
|
@ -279,55 +292,37 @@ class AuthStore:
|
||||||
"""Update refresh token last used information."""
|
"""Update refresh token last used information."""
|
||||||
refresh_token.last_used_at = dt_util.utcnow()
|
refresh_token.last_used_at = dt_util.utcnow()
|
||||||
refresh_token.last_used_ip = remote_ip
|
refresh_token.last_used_ip = remote_ip
|
||||||
if refresh_token.expire_at:
|
|
||||||
refresh_token.expire_at = (
|
|
||||||
refresh_token.last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
|
|
||||||
)
|
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
@callback
|
async def _async_load(self) -> None:
|
||||||
def async_set_expiry(
|
|
||||||
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
|
||||||
) -> None:
|
|
||||||
"""Enable or disable expiry of a refresh token."""
|
|
||||||
if enable_expiry:
|
|
||||||
if refresh_token.expire_at is None:
|
|
||||||
refresh_token.expire_at = (
|
|
||||||
refresh_token.last_used_at or dt_util.utcnow()
|
|
||||||
).timestamp() + REFRESH_TOKEN_EXPIRATION
|
|
||||||
self._async_schedule_save()
|
|
||||||
else:
|
|
||||||
refresh_token.expire_at = None
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_user_credentials_data(
|
|
||||||
self, credentials: models.Credentials, data: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""Update credentials data."""
|
|
||||||
credentials.data = data
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
async def async_load(self) -> None: # noqa: C901
|
|
||||||
"""Load the users."""
|
"""Load the users."""
|
||||||
if self._loaded:
|
async with self._lock:
|
||||||
raise RuntimeError("Auth storage is already loaded")
|
if self._users is not None:
|
||||||
self._loaded = True
|
return
|
||||||
|
await self._async_load_task()
|
||||||
|
|
||||||
dev_reg = dr.async_get(self.hass)
|
async def _async_load_task(self) -> None:
|
||||||
ent_reg = er.async_get(self.hass)
|
"""Load the users."""
|
||||||
data = await self._store.async_load()
|
[ent_reg, dev_reg, data] = await asyncio.gather(
|
||||||
|
self.hass.helpers.entity_registry.async_get_registry(),
|
||||||
|
self.hass.helpers.device_registry.async_get_registry(),
|
||||||
|
self._store.async_load(),
|
||||||
|
)
|
||||||
|
|
||||||
perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
# Make sure that we're not overriding data if 2 loads happened at the
|
||||||
self._perm_lookup = perm_lookup
|
# same time
|
||||||
|
if self._users is not None:
|
||||||
|
return
|
||||||
|
|
||||||
if data is None or not isinstance(data, dict):
|
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
self._set_defaults()
|
self._set_defaults()
|
||||||
return
|
return
|
||||||
|
|
||||||
users: dict[str, models.User] = {}
|
users: dict[str, models.User] = OrderedDict()
|
||||||
groups: dict[str, models.Group] = {}
|
groups: dict[str, models.Group] = OrderedDict()
|
||||||
credentials: dict[str, models.Credentials] = {}
|
credentials: dict[str, models.Credentials] = OrderedDict()
|
||||||
|
|
||||||
# Soft-migrating data as we load. We are going to make sure we have a
|
# Soft-migrating data as we load. We are going to make sure we have a
|
||||||
# read only group and an admin group. There are two states that we can
|
# read only group and an admin group. There are two states that we can
|
||||||
|
@ -454,10 +449,8 @@ class AuthStore:
|
||||||
created_at = dt_util.parse_datetime(rt_dict["created_at"])
|
created_at = dt_util.parse_datetime(rt_dict["created_at"])
|
||||||
if created_at is None:
|
if created_at is None:
|
||||||
getLogger(__name__).error(
|
getLogger(__name__).error(
|
||||||
(
|
|
||||||
"Ignoring refresh token %(id)s with invalid created_at "
|
"Ignoring refresh token %(id)s with invalid created_at "
|
||||||
"%(created_at)s for user_id %(user_id)s"
|
"%(created_at)s for user_id %(user_id)s",
|
||||||
),
|
|
||||||
rt_dict,
|
rt_dict,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
@ -490,35 +483,28 @@ class AuthStore:
|
||||||
jwt_key=rt_dict["jwt_key"],
|
jwt_key=rt_dict["jwt_key"],
|
||||||
last_used_at=last_used_at,
|
last_used_at=last_used_at,
|
||||||
last_used_ip=rt_dict.get("last_used_ip"),
|
last_used_ip=rt_dict.get("last_used_ip"),
|
||||||
expire_at=rt_dict.get("expire_at"),
|
credential=credentials.get(rt_dict.get("credential_id")),
|
||||||
version=rt_dict.get("version"),
|
version=rt_dict.get("version"),
|
||||||
)
|
)
|
||||||
if "credential_id" in rt_dict:
|
|
||||||
token.credential = credentials.get(rt_dict["credential_id"])
|
|
||||||
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
||||||
|
|
||||||
self._groups = groups
|
self._groups = groups
|
||||||
self._users = users
|
self._users = users
|
||||||
self._build_token_id_to_user_id()
|
|
||||||
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _build_token_id_to_user_id(self) -> None:
|
def _async_schedule_save(self) -> None:
|
||||||
"""Build a map of token id to user id."""
|
|
||||||
self._token_id_to_user_id = {
|
|
||||||
token_id: user_id
|
|
||||||
for user_id, user in self._users.items()
|
|
||||||
for token_id in user.refresh_tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
|
|
||||||
"""Save users."""
|
"""Save users."""
|
||||||
self._store.async_delay_save(self._data_to_save, delay)
|
if self._users is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._store.async_delay_save(self._data_to_save, 1)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
|
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
|
||||||
"""Return the data to store."""
|
"""Return the data to store."""
|
||||||
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{
|
{
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
|
@ -566,16 +552,13 @@ class AuthStore:
|
||||||
"client_icon": refresh_token.client_icon,
|
"client_icon": refresh_token.client_icon,
|
||||||
"token_type": refresh_token.token_type,
|
"token_type": refresh_token.token_type,
|
||||||
"created_at": refresh_token.created_at.isoformat(),
|
"created_at": refresh_token.created_at.isoformat(),
|
||||||
"access_token_expiration": (
|
"access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
|
||||||
refresh_token.access_token_expiration.total_seconds()
|
|
||||||
),
|
|
||||||
"token": refresh_token.token,
|
"token": refresh_token.token,
|
||||||
"jwt_key": refresh_token.jwt_key,
|
"jwt_key": refresh_token.jwt_key,
|
||||||
"last_used_at": refresh_token.last_used_at.isoformat()
|
"last_used_at": refresh_token.last_used_at.isoformat()
|
||||||
if refresh_token.last_used_at
|
if refresh_token.last_used_at
|
||||||
else None,
|
else None,
|
||||||
"last_used_ip": refresh_token.last_used_ip,
|
"last_used_ip": refresh_token.last_used_ip,
|
||||||
"expire_at": refresh_token.expire_at,
|
|
||||||
"credential_id": refresh_token.credential.id
|
"credential_id": refresh_token.credential.id
|
||||||
if refresh_token.credential
|
if refresh_token.credential
|
||||||
else None,
|
else None,
|
||||||
|
@ -594,9 +577,9 @@ class AuthStore:
|
||||||
|
|
||||||
def _set_defaults(self) -> None:
|
def _set_defaults(self) -> None:
|
||||||
"""Set default values for auth store."""
|
"""Set default values for auth store."""
|
||||||
self._users = {}
|
self._users = OrderedDict()
|
||||||
|
|
||||||
groups: dict[str, models.Group] = {}
|
groups: dict[str, models.Group] = OrderedDict()
|
||||||
admin_group = _system_admin_group()
|
admin_group = _system_admin_group()
|
||||||
groups[admin_group.id] = admin_group
|
groups[admin_group.id] = admin_group
|
||||||
user_group = _system_user_group()
|
user_group = _system_user_group()
|
||||||
|
@ -604,7 +587,6 @@ class AuthStore:
|
||||||
read_only_group = _system_read_only_group()
|
read_only_group = _system_read_only_group()
|
||||||
groups[read_only_group.id] = read_only_group
|
groups[read_only_group.id] = read_only_group
|
||||||
self._groups = groups
|
self._groups = groups
|
||||||
self._build_token_id_to_user_id()
|
|
||||||
|
|
||||||
|
|
||||||
def _system_admin_group() -> models.Group:
|
def _system_admin_group() -> models.Group:
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
"""Constants for the auth module."""
|
"""Constants for the auth module."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||||
REFRESH_TOKEN_EXPIRATION = timedelta(days=90).total_seconds()
|
|
||||||
|
|
||||||
GROUP_ID_ADMIN = "system-admin"
|
GROUP_ID_ADMIN = "system-admin"
|
||||||
GROUP_ID_USER = "system-users"
|
GROUP_ID_USER = "system-users"
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
"""Provide a wrapper around JWT that caches decoding tokens.
|
|
||||||
|
|
||||||
Since we decode the same tokens over and over again
|
|
||||||
we can cache the result of the decode of valid tokens
|
|
||||||
to speed up the process.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from functools import lru_cache, partial
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from jwt import DecodeError, PyJWS, PyJWT
|
|
||||||
|
|
||||||
from homeassistant.util.json import json_loads
|
|
||||||
|
|
||||||
JWT_TOKEN_CACHE_SIZE = 16
|
|
||||||
MAX_TOKEN_SIZE = 8192
|
|
||||||
|
|
||||||
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss")
|
|
||||||
|
|
||||||
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
|
|
||||||
"require": []
|
|
||||||
}
|
|
||||||
_NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
|
|
||||||
|
|
||||||
|
|
||||||
class _PyJWSWithLoadCache(PyJWS):
|
|
||||||
"""PyJWS with a dedicated load implementation."""
|
|
||||||
|
|
||||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
|
||||||
# We only ever have a global instance of this class
|
|
||||||
# so we do not have to worry about the LRU growing
|
|
||||||
# each time we create a new instance.
|
|
||||||
def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict, bytes]:
|
|
||||||
"""Load a JWS."""
|
|
||||||
return super()._load(jwt)
|
|
||||||
|
|
||||||
|
|
||||||
_jws = _PyJWSWithLoadCache()
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
|
||||||
def _decode_payload(json_payload: str) -> dict[str, Any]:
|
|
||||||
"""Decode the payload from a JWS dictionary."""
|
|
||||||
try:
|
|
||||||
payload = json_loads(json_payload)
|
|
||||||
except ValueError as err:
|
|
||||||
raise DecodeError(f"Invalid payload string: {err}") from err
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
raise DecodeError("Invalid payload string: must be a json object")
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
class _PyJWTWithVerify(PyJWT):
|
|
||||||
"""PyJWT with a fast decode implementation."""
|
|
||||||
|
|
||||||
def decode_payload(
|
|
||||||
self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Decode a JWT's payload."""
|
|
||||||
if len(jwt) > MAX_TOKEN_SIZE:
|
|
||||||
# Avoid caching impossible tokens
|
|
||||||
raise DecodeError("Token too large")
|
|
||||||
return _decode_payload(
|
|
||||||
_jws.decode_complete(
|
|
||||||
jwt=jwt,
|
|
||||||
key=key,
|
|
||||||
algorithms=algorithms,
|
|
||||||
options=options,
|
|
||||||
)["payload"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def verify_and_decode(
|
|
||||||
self,
|
|
||||||
jwt: str,
|
|
||||||
key: str,
|
|
||||||
algorithms: list[str],
|
|
||||||
issuer: str | None = None,
|
|
||||||
leeway: float | timedelta = 0,
|
|
||||||
options: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Verify a JWT's signature and claims."""
|
|
||||||
merged_options = {**_VERIFY_OPTIONS, **(options or {})}
|
|
||||||
payload = self.decode_payload(
|
|
||||||
jwt=jwt,
|
|
||||||
key=key,
|
|
||||||
options=merged_options,
|
|
||||||
algorithms=algorithms,
|
|
||||||
)
|
|
||||||
# These should never be missing since we verify them
|
|
||||||
# but this is an additional safeguard to make sure
|
|
||||||
# nothing slips through.
|
|
||||||
assert "exp" in payload, "exp claim is required"
|
|
||||||
assert "iat" in payload, "iat claim is required"
|
|
||||||
self._validate_claims(
|
|
||||||
payload=payload,
|
|
||||||
options=merged_options,
|
|
||||||
issuer=issuer,
|
|
||||||
leeway=leeway,
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
_jwt = _PyJWTWithVerify()
|
|
||||||
verify_and_decode = _jwt.verify_and_decode
|
|
||||||
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
|
|
||||||
partial(
|
|
||||||
_jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"unverified_hs256_token_decode",
|
|
||||||
"verify_and_decode",
|
|
||||||
]
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Pluggable auth modules for Home Assistant."""
|
"""Pluggable auth modules for Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import types
|
import types
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -14,11 +14,9 @@ from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.importlib import async_import_module
|
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.util.hass_dict import HassKey
|
|
||||||
|
|
||||||
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
|
MULTI_FACTOR_AUTH_MODULES = Registry()
|
||||||
|
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -30,7 +28,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_REQS: HassKey[set[str]] = HassKey("mfa_auth_module_reqs_processed")
|
DATA_REQS = "mfa_auth_module_reqs_processed"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -52,17 +50,17 @@ class MultiFactorAuthModule:
|
||||||
|
|
||||||
Default is same as type
|
Default is same as type
|
||||||
"""
|
"""
|
||||||
return self.config.get(CONF_ID, self.type) # type: ignore[no-any-return]
|
return self.config.get(CONF_ID, self.type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""Return type of the module."""
|
"""Return type of the module."""
|
||||||
return self.config[CONF_TYPE] # type: ignore[no-any-return]
|
return self.config[CONF_TYPE] # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the auth module."""
|
"""Return the name of the auth module."""
|
||||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE) # type: ignore[no-any-return]
|
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||||
|
|
||||||
# Implement by extending class
|
# Implement by extending class
|
||||||
|
|
||||||
|
@ -118,7 +116,9 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
result = await self._auth_module.async_setup_user(self._user_id, user_input)
|
result = await self._auth_module.async_setup_user(self._user_id, user_input)
|
||||||
return self.async_create_entry(data={"result": result})
|
return self.async_create_entry(
|
||||||
|
title=self._auth_module.name, data={"result": result}
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init", data_schema=self._setup_schema, errors=errors
|
step_id="init", data_schema=self._setup_schema, errors=errors
|
||||||
|
@ -129,7 +129,7 @@ async def auth_mfa_module_from_config(
|
||||||
hass: HomeAssistant, config: dict[str, Any]
|
hass: HomeAssistant, config: dict[str, Any]
|
||||||
) -> MultiFactorAuthModule:
|
) -> MultiFactorAuthModule:
|
||||||
"""Initialize an auth module from a config."""
|
"""Initialize an auth module from a config."""
|
||||||
module_name: str = config[CONF_TYPE]
|
module_name = config[CONF_TYPE]
|
||||||
module = await _load_mfa_module(hass, module_name)
|
module = await _load_mfa_module(hass, module_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -142,7 +142,7 @@ async def auth_mfa_module_from_config(
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config)
|
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType:
|
async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType:
|
||||||
|
@ -150,7 +150,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul
|
||||||
module_path = f"homeassistant.auth.mfa_modules.{module_name}"
|
module_path = f"homeassistant.auth.mfa_modules.{module_name}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = await async_import_module(hass, module_path)
|
module = importlib.import_module(module_path)
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
|
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
|
@ -166,6 +166,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul
|
||||||
|
|
||||||
processed = hass.data[DATA_REQS] = set()
|
processed = hass.data[DATA_REQS] = set()
|
||||||
|
|
||||||
|
# https://github.com/python/mypy/issues/1424
|
||||||
await requirements.async_process_requirements(
|
await requirements.async_process_requirements(
|
||||||
hass, module_path, module.REQUIREMENTS
|
hass, module_path, module.REQUIREMENTS
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Example auth module."""
|
"""Example auth module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
Sending HOTP through notify service
|
Sending HOTP through notify service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -18,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||||
|
@ -27,7 +25,7 @@ from . import (
|
||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.8.0"]
|
REQUIREMENTS = ["pyotp==2.6.0"]
|
||||||
|
|
||||||
CONF_MESSAGE = "message"
|
CONF_MESSAGE = "message"
|
||||||
|
|
||||||
|
@ -88,7 +86,7 @@ class NotifySetting:
|
||||||
target: str | None = attr.ib(default=None)
|
target: str | None = attr.ib(default=None)
|
||||||
|
|
||||||
|
|
||||||
type _UsersDict = dict[str, NotifySetting]
|
_UsersDict = dict[str, NotifySetting]
|
||||||
|
|
||||||
|
|
||||||
@MULTI_FACTOR_AUTH_MODULES.register("notify")
|
@MULTI_FACTOR_AUTH_MODULES.register("notify")
|
||||||
|
@ -101,8 +99,8 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
super().__init__(hass, config)
|
super().__init__(hass, config)
|
||||||
self._user_settings: _UsersDict | None = None
|
self._user_settings: _UsersDict | None = None
|
||||||
self._user_store = Store[dict[str, dict[str, Any]]](
|
self._user_store = hass.helpers.storage.Store(
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||||
)
|
)
|
||||||
self._include = config.get(CONF_INCLUDE, [])
|
self._include = config.get(CONF_INCLUDE, [])
|
||||||
self._exclude = config.get(CONF_EXCLUDE, [])
|
self._exclude = config.get(CONF_EXCLUDE, [])
|
||||||
|
@ -121,7 +119,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||||
return
|
return
|
||||||
|
|
||||||
if (data := await self._user_store.async_load()) is None:
|
if (data := await self._user_store.async_load()) is None:
|
||||||
data = cast(dict[str, dict[str, Any]], {STORAGE_USERS: {}})
|
data = {STORAGE_USERS: {}}
|
||||||
|
|
||||||
self._user_settings = {
|
self._user_settings = {
|
||||||
user_id: NotifySetting(**setting)
|
user_id: NotifySetting(**setting)
|
||||||
|
@ -153,7 +151,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||||
"""Return list of notify services."""
|
"""Return list of notify services."""
|
||||||
unordered_services = set()
|
unordered_services = set()
|
||||||
|
|
||||||
for service in self.hass.services.async_services_for_domain("notify"):
|
for service in self.hass.services.async_services().get("notify", {}):
|
||||||
if service not in self._exclude:
|
if service not in self._exclude:
|
||||||
unordered_services.add(service)
|
unordered_services.add(service)
|
||||||
|
|
||||||
|
@ -253,7 +251,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||||
|
|
||||||
await self.async_notify(
|
await self.async_notify(
|
||||||
code,
|
code,
|
||||||
notify_setting.notify_service, # type: ignore[arg-type]
|
notify_setting.notify_service, # type: ignore
|
||||||
notify_setting.target,
|
notify_setting.target,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -321,7 +319,6 @@ class NotifySetupFlow(SetupFlow):
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
hass = self._auth_module.hass
|
hass = self._auth_module.hass
|
||||||
assert self._secret and self._count
|
|
||||||
if user_input:
|
if user_input:
|
||||||
verified = await hass.async_add_executor_job(
|
verified = await hass.async_add_executor_job(
|
||||||
_verify_otp, self._secret, user_input["code"], self._count
|
_verify_otp, self._secret, user_input["code"], self._count
|
||||||
|
@ -331,11 +328,12 @@ class NotifySetupFlow(SetupFlow):
|
||||||
self._user_id,
|
self._user_id,
|
||||||
{"notify_service": self._notify_service, "target": self._target},
|
{"notify_service": self._notify_service, "target": self._target},
|
||||||
)
|
)
|
||||||
return self.async_create_entry(data={})
|
return self.async_create_entry(title=self._auth_module.name, data={})
|
||||||
|
|
||||||
errors["base"] = "invalid_code"
|
errors["base"] = "invalid_code"
|
||||||
|
|
||||||
# generate code every time, no retry logic
|
# generate code every time, no retry logic
|
||||||
|
assert self._secret and self._count
|
||||||
code = await hass.async_add_executor_job(
|
code = await hass.async_add_executor_job(
|
||||||
_generate_otp, self._secret, self._count
|
_generate_otp, self._secret, self._count
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
"""Time-based One Time Password auth module."""
|
"""Time-based One Time Password auth module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.auth.models import User
|
from homeassistant.auth.models import User
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||||
|
@ -20,7 +18,7 @@ from . import (
|
||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.8.0", "PyQRCode==1.2.1"]
|
REQUIREMENTS = ["pyotp==2.6.0", "PyQRCode==1.2.1"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
@ -48,10 +46,8 @@ def _generate_qr_code(data: str) -> str:
|
||||||
.decode("ascii")
|
.decode("ascii")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
.replace(
|
.replace(
|
||||||
(
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg"'
|
'<svg xmlns="http://www.w3.org/2000/svg"',
|
||||||
),
|
|
||||||
"<svg",
|
"<svg",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -80,8 +76,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
super().__init__(hass, config)
|
super().__init__(hass, config)
|
||||||
self._users: dict[str, str] | None = None
|
self._users: dict[str, str] | None = None
|
||||||
self._user_store = Store[dict[str, dict[str, str]]](
|
self._user_store = hass.helpers.storage.Store(
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||||
)
|
)
|
||||||
self._init_lock = asyncio.Lock()
|
self._init_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
@ -97,13 +93,13 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
return
|
return
|
||||||
|
|
||||||
if (data := await self._user_store.async_load()) is None:
|
if (data := await self._user_store.async_load()) is None:
|
||||||
data = cast(dict[str, dict[str, str]], {STORAGE_USERS: {}})
|
data = {STORAGE_USERS: {}}
|
||||||
|
|
||||||
self._users = data.get(STORAGE_USERS, {})
|
self._users = data.get(STORAGE_USERS, {})
|
||||||
|
|
||||||
async def _async_save(self) -> None:
|
async def _async_save(self) -> None:
|
||||||
"""Save data."""
|
"""Save data."""
|
||||||
await self._user_store.async_save({STORAGE_USERS: self._users or {}})
|
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||||
|
|
||||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||||
"""Create a ota_secret for user."""
|
"""Create a ota_secret for user."""
|
||||||
|
@ -111,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
|
|
||||||
ota_secret: str = secret or pyotp.random_base32()
|
ota_secret: str = secret or pyotp.random_base32()
|
||||||
|
|
||||||
self._users[user_id] = ota_secret # type: ignore[index]
|
self._users[user_id] = ota_secret # type: ignore
|
||||||
return ota_secret
|
return ota_secret
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||||
|
@ -140,7 +136,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
||||||
if self._users.pop(user_id, None): # type: ignore[union-attr]
|
if self._users.pop(user_id, None): # type: ignore
|
||||||
await self._async_save()
|
await self._async_save()
|
||||||
|
|
||||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||||
|
@ -148,7 +144,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
||||||
return user_id in self._users # type: ignore[operator]
|
return user_id in self._users # type: ignore
|
||||||
|
|
||||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
|
@ -165,7 +161,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
"""Validate two factor authentication code."""
|
"""Validate two factor authentication code."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
if (ota_secret := self._users.get(user_id)) is None: # type: ignore
|
||||||
# even we cannot find user, we still do verify
|
# even we cannot find user, we still do verify
|
||||||
# to make timing the same as if user was found.
|
# to make timing the same as if user was found.
|
||||||
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
||||||
|
@ -209,7 +205,9 @@ class TotpSetupFlow(SetupFlow):
|
||||||
result = await self._auth_module.async_setup_user(
|
result = await self._auth_module.async_setup_user(
|
||||||
self._user_id, {"secret": self._ota_secret}
|
self._user_id, {"secret": self._ota_secret}
|
||||||
)
|
)
|
||||||
return self.async_create_entry(data={"result": result})
|
return self.async_create_entry(
|
||||||
|
title=self._auth_module.name, data={"result": result}
|
||||||
|
)
|
||||||
|
|
||||||
errors["base"] = "invalid_code"
|
errors["base"] = "invalid_code"
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
"""Auth models."""
|
"""Auth models."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, NamedTuple
|
from typing import NamedTuple
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from attr import Attribute
|
|
||||||
from attr.setters import validate
|
|
||||||
from propcache import cached_property
|
|
||||||
|
|
||||||
from homeassistant.const import __version__
|
from homeassistant.const import __version__
|
||||||
from homeassistant.data_entry_flow import FlowContext, FlowResult
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import permissions as perm_mdl
|
from . import permissions as perm_mdl
|
||||||
|
@ -25,17 +19,6 @@ TOKEN_TYPE_SYSTEM = "system"
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||||
|
|
||||||
|
|
||||||
class AuthFlowContext(FlowContext, total=False):
|
|
||||||
"""Typed context dict for auth flow."""
|
|
||||||
|
|
||||||
credential_only: bool
|
|
||||||
ip_address: IPv4Address | IPv6Address
|
|
||||||
redirect_uri: str
|
|
||||||
|
|
||||||
|
|
||||||
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class Group:
|
class Group:
|
||||||
"""A group."""
|
"""A group."""
|
||||||
|
@ -46,27 +29,19 @@ class Group:
|
||||||
system_generated: bool = attr.ib(default=False)
|
system_generated: bool = attr.ib(default=False)
|
||||||
|
|
||||||
|
|
||||||
def _handle_permissions_change(self: User, user_attr: Attribute, new: Any) -> Any:
|
@attr.s(slots=True)
|
||||||
"""Handle a change to a permissions."""
|
|
||||||
self.invalidate_cache()
|
|
||||||
return validate(self, user_attr, new)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=False)
|
|
||||||
class User:
|
class User:
|
||||||
"""A user."""
|
"""A user."""
|
||||||
|
|
||||||
name: str | None = attr.ib()
|
name: str | None = attr.ib()
|
||||||
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
|
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
|
||||||
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||||
is_owner: bool = attr.ib(default=False, on_setattr=_handle_permissions_change)
|
is_owner: bool = attr.ib(default=False)
|
||||||
is_active: bool = attr.ib(default=False, on_setattr=_handle_permissions_change)
|
is_active: bool = attr.ib(default=False)
|
||||||
system_generated: bool = attr.ib(default=False)
|
system_generated: bool = attr.ib(default=False)
|
||||||
local_only: bool = attr.ib(default=False)
|
local_only: bool = attr.ib(default=False)
|
||||||
|
|
||||||
groups: list[Group] = attr.ib(
|
groups: list[Group] = attr.ib(factory=list, eq=False, order=False)
|
||||||
factory=list, eq=False, order=False, on_setattr=_handle_permissions_change
|
|
||||||
)
|
|
||||||
|
|
||||||
# List of credentials of a user.
|
# List of credentials of a user.
|
||||||
credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False)
|
credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False)
|
||||||
|
@ -76,27 +51,40 @@ class User:
|
||||||
factory=dict, eq=False, order=False
|
factory=dict, eq=False, order=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
_permissions: perm_mdl.PolicyPermissions | None = attr.ib(
|
||||||
|
init=False,
|
||||||
|
eq=False,
|
||||||
|
order=False,
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
def permissions(self) -> perm_mdl.AbstractPermissions:
|
def permissions(self) -> perm_mdl.AbstractPermissions:
|
||||||
"""Return permissions object for user."""
|
"""Return permissions object for user."""
|
||||||
if self.is_owner:
|
if self.is_owner:
|
||||||
return perm_mdl.OwnerPermissions
|
return perm_mdl.OwnerPermissions
|
||||||
return perm_mdl.PolicyPermissions(
|
|
||||||
|
if self._permissions is not None:
|
||||||
|
return self._permissions
|
||||||
|
|
||||||
|
self._permissions = perm_mdl.PolicyPermissions(
|
||||||
perm_mdl.merge_policies([group.policy for group in self.groups]),
|
perm_mdl.merge_policies([group.policy for group in self.groups]),
|
||||||
self.perm_lookup,
|
self.perm_lookup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
return self._permissions
|
||||||
|
|
||||||
|
@property
|
||||||
def is_admin(self) -> bool:
|
def is_admin(self) -> bool:
|
||||||
"""Return if user is part of the admin group."""
|
"""Return if user is part of the admin group."""
|
||||||
return self.is_owner or (
|
if self.is_owner:
|
||||||
self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
|
return True
|
||||||
)
|
|
||||||
|
|
||||||
def invalidate_cache(self) -> None:
|
return self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
|
||||||
"""Invalidate permission and is_admin cache."""
|
|
||||||
for attr_to_invalidate in ("permissions", "is_admin"):
|
def invalidate_permission_cache(self) -> None:
|
||||||
self.__dict__.pop(attr_to_invalidate, None)
|
"""Invalidate permission cache."""
|
||||||
|
self._permissions = None
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
|
@ -122,8 +110,6 @@ class RefreshToken:
|
||||||
last_used_at: datetime | None = attr.ib(default=None)
|
last_used_at: datetime | None = attr.ib(default=None)
|
||||||
last_used_ip: str | None = attr.ib(default=None)
|
last_used_ip: str | None = attr.ib(default=None)
|
||||||
|
|
||||||
expire_at: float | None = attr.ib(default=None)
|
|
||||||
|
|
||||||
credential: Credentials | None = attr.ib(default=None)
|
credential: Credentials | None = attr.ib(default=None)
|
||||||
|
|
||||||
version: str | None = attr.ib(default=__version__)
|
version: str | None = attr.ib(default=__version__)
|
||||||
|
@ -148,5 +134,3 @@ class UserMeta(NamedTuple):
|
||||||
|
|
||||||
name: str | None
|
name: str | None
|
||||||
is_active: bool
|
is_active: bool
|
||||||
group: str | None = None
|
|
||||||
local_only: bool | None = None
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""Permissions for Home Assistant."""
|
"""Permissions for Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class PolicyPermissions(AbstractPermissions):
|
||||||
"""Return a function that can test entity access."""
|
"""Return a function that can test entity access."""
|
||||||
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
|
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
"""Equals check."""
|
"""Equals check."""
|
||||||
return isinstance(other, PolicyPermissions) and other._policy == self._policy
|
return isinstance(other, PolicyPermissions) and other._policy == self._policy
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Permission constants."""
|
"""Permission constants."""
|
||||||
|
|
||||||
CAT_ENTITIES = "entities"
|
CAT_ENTITIES = "entities"
|
||||||
CAT_CONFIG_ENTRIES = "config_entries"
|
CAT_CONFIG_ENTRIES = "config_entries"
|
||||||
SUBCAT_ALL = "all"
|
SUBCAT_ALL = "all"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Entity permissions."""
|
"""Entity permissions."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -48,7 +47,7 @@ def _lookup_domain(
|
||||||
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
|
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
|
||||||
) -> ValueType | None:
|
) -> ValueType | None:
|
||||||
"""Look up entity permissions by domain."""
|
"""Look up entity permissions by domain."""
|
||||||
return domains_dict.get(entity_id.partition(".")[0])
|
return domains_dict.get(entity_id.split(".", 1)[0])
|
||||||
|
|
||||||
|
|
||||||
def _lookup_area(
|
def _lookup_area(
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
"""Permission for events."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Final
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
|
||||||
EVENT_COMPONENT_LOADED,
|
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
|
||||||
EVENT_LOVELACE_UPDATED,
|
|
||||||
EVENT_PANELS_UPDATED,
|
|
||||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
|
||||||
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED,
|
|
||||||
EVENT_SERVICE_REGISTERED,
|
|
||||||
EVENT_SERVICE_REMOVED,
|
|
||||||
EVENT_SHOPPING_LIST_UPDATED,
|
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
EVENT_THEMES_UPDATED,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
|
||||||
from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED
|
|
||||||
from homeassistant.util.event_type import EventType
|
|
||||||
|
|
||||||
# These are events that do not contain any sensitive data
|
|
||||||
# Except for state_changed, which is handled accordingly.
|
|
||||||
SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
|
||||||
EVENT_AREA_REGISTRY_UPDATED,
|
|
||||||
EVENT_COMPONENT_LOADED,
|
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
|
||||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
|
||||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
|
||||||
EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
|
|
||||||
EVENT_LOVELACE_UPDATED,
|
|
||||||
EVENT_PANELS_UPDATED,
|
|
||||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
|
||||||
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED,
|
|
||||||
EVENT_SERVICE_REGISTERED,
|
|
||||||
EVENT_SERVICE_REMOVED,
|
|
||||||
EVENT_SHOPPING_LIST_UPDATED,
|
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
EVENT_THEMES_UPDATED,
|
|
||||||
EVENT_LABEL_REGISTRY_UPDATED,
|
|
||||||
EVENT_CATEGORY_REGISTRY_UPDATED,
|
|
||||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Merging of policies."""
|
"""Merging of policies."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -58,7 +57,10 @@ def _merge_policies(sources: list[CategoryType]) -> CategoryType:
|
||||||
continue
|
continue
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
|
|
||||||
key_sources = [src.get(key) for src in sources if isinstance(src, dict)]
|
key_sources = []
|
||||||
|
for src in sources:
|
||||||
|
if isinstance(src, dict):
|
||||||
|
key_sources.append(src.get(key))
|
||||||
|
|
||||||
policy[key] = _merge_policies(key_sources)
|
policy[key] = _merge_policies(key_sources)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Models for permissions."""
|
"""Models for permissions."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -7,12 +6,15 @@ from typing import TYPE_CHECKING
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import (
|
||||||
|
device_registry as dev_reg,
|
||||||
|
entity_registry as ent_reg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class PermissionLookup:
|
class PermissionLookup:
|
||||||
"""Class to hold data for permission lookups."""
|
"""Class to hold data for permission lookups."""
|
||||||
|
|
||||||
entity_registry: er.EntityRegistry = attr.ib()
|
entity_registry: ent_reg.EntityRegistry = attr.ib()
|
||||||
device_registry: dr.DeviceRegistry = attr.ib()
|
device_registry: dev_reg.DeviceRegistry = attr.ib()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""System policies."""
|
"""System policies."""
|
||||||
|
|
||||||
from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL
|
from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL
|
||||||
|
|
||||||
ADMIN_POLICY = {CAT_ENTITIES: True}
|
ADMIN_POLICY = {CAT_ENTITIES: True}
|
||||||
|
|
|
@ -1,27 +1,29 @@
|
||||||
"""Common code for permissions."""
|
"""Common code for permissions."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||||
|
|
||||||
type ValueType = (
|
ValueType = Union[
|
||||||
# Example: entities.all = { read: true, control: true }
|
# Example: entities.all = { read: true, control: true }
|
||||||
Mapping[str, bool] | bool | None
|
Mapping[str, bool],
|
||||||
)
|
bool,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
# Example: entities.domains = { light: … }
|
# Example: entities.domains = { light: … }
|
||||||
type SubCategoryDict = Mapping[str, ValueType]
|
SubCategoryDict = Mapping[str, ValueType]
|
||||||
|
|
||||||
type SubCategoryType = SubCategoryDict | bool | None
|
SubCategoryType = Union[SubCategoryDict, bool, None]
|
||||||
|
|
||||||
type CategoryType = (
|
CategoryType = Union[
|
||||||
# Example: entities.domains
|
# Example: entities.domains
|
||||||
Mapping[str, SubCategoryType]
|
Mapping[str, SubCategoryType],
|
||||||
# Example: entities.all
|
# Example: entities.all
|
||||||
| Mapping[str, ValueType]
|
Mapping[str, ValueType],
|
||||||
| bool
|
bool,
|
||||||
| None
|
None,
|
||||||
)
|
]
|
||||||
|
|
||||||
# Example: { entities: … }
|
# Example: { entities: … }
|
||||||
type PolicyType = Mapping[str, CategoryType]
|
PolicyType = Mapping[str, CategoryType]
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
"""Helpers to deal with permissions."""
|
"""Helpers to deal with permissions."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import cast
|
from typing import Optional, cast
|
||||||
|
|
||||||
from .const import SUBCAT_ALL
|
from .const import SUBCAT_ALL
|
||||||
from .models import PermissionLookup
|
from .models import PermissionLookup
|
||||||
from .types import CategoryType, SubCategoryDict, ValueType
|
from .types import CategoryType, SubCategoryDict, ValueType
|
||||||
|
|
||||||
type LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]]
|
||||||
type SubCatLookupType = dict[str, LookupFunc]
|
SubCatLookupType = dict[str, LookupFunc]
|
||||||
|
|
||||||
|
|
||||||
def lookup_all(
|
def lookup_all(
|
||||||
|
@ -110,4 +109,4 @@ def test_all(policy: CategoryType, key: str) -> bool:
|
||||||
if not isinstance(all_policy, dict):
|
if not isinstance(all_policy, dict):
|
||||||
return bool(all_policy)
|
return bool(all_policy)
|
||||||
|
|
||||||
return all_policy.get(key, False) # type: ignore[no-any-return]
|
return all_policy.get(key, False)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""Auth providers for Home Assistant."""
|
"""Auth providers for Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import types
|
import types
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -10,31 +10,22 @@ from typing import Any
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from homeassistant import requirements
|
from homeassistant import data_entry_flow, requirements
|
||||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.data_entry_flow import FlowHandler
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.importlib import async_import_module
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.util.hass_dict import HassKey
|
|
||||||
|
|
||||||
from ..auth_store import AuthStore
|
from ..auth_store import AuthStore
|
||||||
from ..const import MFA_SESSION_EXPIRATION
|
from ..const import MFA_SESSION_EXPIRATION
|
||||||
from ..models import (
|
from ..models import Credentials, RefreshToken, User, UserMeta
|
||||||
AuthFlowContext,
|
|
||||||
AuthFlowResult,
|
|
||||||
Credentials,
|
|
||||||
RefreshToken,
|
|
||||||
User,
|
|
||||||
UserMeta,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
|
DATA_REQS = "auth_prov_reqs_processed"
|
||||||
|
|
||||||
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()
|
AUTH_PROVIDERS = Registry()
|
||||||
|
|
||||||
AUTH_PROVIDER_SCHEMA = vol.Schema(
|
AUTH_PROVIDER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -71,12 +62,12 @@ class AuthProvider:
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""Return type of the provider."""
|
"""Return type of the provider."""
|
||||||
return self.config[CONF_TYPE] # type: ignore[no-any-return]
|
return self.config[CONF_TYPE] # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the auth provider."""
|
"""Return the name of the auth provider."""
|
||||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE) # type: ignore[no-any-return]
|
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def support_mfa(self) -> bool:
|
def support_mfa(self) -> bool:
|
||||||
|
@ -105,7 +96,7 @@ class AuthProvider:
|
||||||
|
|
||||||
# Implement by extending class
|
# Implement by extending class
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
"""Return the data flow for logging in with auth provider.
|
"""Return the data flow for logging in with auth provider.
|
||||||
|
|
||||||
Auth provider should extend LoginFlow and return an instance.
|
Auth provider should extend LoginFlow and return an instance.
|
||||||
|
@ -145,7 +136,7 @@ async def auth_provider_from_config(
|
||||||
hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
|
hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
|
||||||
) -> AuthProvider:
|
) -> AuthProvider:
|
||||||
"""Initialize an auth provider from a config."""
|
"""Initialize an auth provider from a config."""
|
||||||
provider_name: str = config[CONF_TYPE]
|
provider_name = config[CONF_TYPE]
|
||||||
module = await load_auth_provider_module(hass, provider_name)
|
module = await load_auth_provider_module(hass, provider_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -158,7 +149,7 @@ async def auth_provider_from_config(
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def load_auth_provider_module(
|
async def load_auth_provider_module(
|
||||||
|
@ -166,9 +157,7 @@ async def load_auth_provider_module(
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
"""Load an auth provider."""
|
"""Load an auth provider."""
|
||||||
try:
|
try:
|
||||||
module = await async_import_module(
|
module = importlib.import_module(f"homeassistant.auth.providers.{provider}")
|
||||||
hass, f"homeassistant.auth.providers.{provider}"
|
|
||||||
)
|
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
|
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
|
@ -192,11 +181,9 @@ async def load_auth_provider_module(
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
_flow_result = AuthFlowResult
|
|
||||||
|
|
||||||
def __init__(self, auth_provider: AuthProvider) -> None:
|
def __init__(self, auth_provider: AuthProvider) -> None:
|
||||||
"""Initialize the login flow."""
|
"""Initialize the login flow."""
|
||||||
self._auth_provider = auth_provider
|
self._auth_provider = auth_provider
|
||||||
|
@ -210,7 +197,7 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the first step of login flow.
|
"""Handle the first step of login flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input is None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
|
@ -220,7 +207,7 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
|
|
||||||
async def async_step_select_mfa_module(
|
async def async_step_select_mfa_module(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of select mfa module."""
|
"""Handle the step of select mfa module."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
@ -245,7 +232,7 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
|
|
||||||
async def async_step_mfa(
|
async def async_step_mfa(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of mfa validation."""
|
"""Handle the step of mfa validation."""
|
||||||
assert self.credential
|
assert self.credential
|
||||||
assert self.user
|
assert self.user
|
||||||
|
@ -263,7 +250,9 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
auth_module, "async_initialize_login_mfa_step"
|
auth_module, "async_initialize_login_mfa_step"
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
await auth_module.async_initialize_login_mfa_step( # type: ignore
|
||||||
|
self.user.id
|
||||||
|
)
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
_LOGGER.exception("Error initializing MFA step")
|
_LOGGER.exception("Error initializing MFA step")
|
||||||
return self.async_abort(reason="unknown_error")
|
return self.async_abort(reason="unknown_error")
|
||||||
|
@ -283,7 +272,7 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
if not errors:
|
if not errors:
|
||||||
return await self.async_finish(self.credential)
|
return await self.async_finish(self.credential)
|
||||||
|
|
||||||
description_placeholders: dict[str, str] = {
|
description_placeholders: dict[str, str | None] = {
|
||||||
"mfa_module_name": auth_module.name,
|
"mfa_module_name": auth_module.name,
|
||||||
"mfa_module_id": auth_module.id,
|
"mfa_module_id": auth_module.id,
|
||||||
}
|
}
|
||||||
|
@ -295,6 +284,6 @@ class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_finish(self, flow_result: Any) -> AuthFlowResult:
|
async def async_finish(self, flow_result: Any) -> FlowResult:
|
||||||
"""Handle the pass of login flow."""
|
"""Handle the pass of login flow."""
|
||||||
return self.async_create_entry(data=flow_result)
|
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Auth provider that validates credentials via an external command."""
|
"""Auth provider that validates credentials via an external command."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -11,10 +10,11 @@ from typing import Any, cast
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_COMMAND
|
from homeassistant.const import CONF_COMMAND
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
CONF_ARGS = "args"
|
CONF_ARGS = "args"
|
||||||
CONF_META = "meta"
|
CONF_META = "meta"
|
||||||
|
@ -44,11 +44,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||||
DEFAULT_TITLE = "Command Line Authentication"
|
DEFAULT_TITLE = "Command Line Authentication"
|
||||||
|
|
||||||
# which keys to accept from a program's stdout
|
# which keys to accept from a program's stdout
|
||||||
ALLOWED_META_KEYS = (
|
ALLOWED_META_KEYS = ("name",)
|
||||||
"name",
|
|
||||||
"group",
|
|
||||||
"local_only",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Extend parent's __init__.
|
"""Extend parent's __init__.
|
||||||
|
@ -59,7 +55,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._user_meta: dict[str, dict[str, Any]] = {}
|
self._user_meta: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return CommandLineLoginFlow(self)
|
return CommandLineLoginFlow(self)
|
||||||
|
|
||||||
|
@ -72,7 +68,6 @@ class CommandLineAuthProvider(AuthProvider):
|
||||||
*self.config[CONF_ARGS],
|
*self.config[CONF_ARGS],
|
||||||
env=env,
|
env=env,
|
||||||
stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None,
|
stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None,
|
||||||
close_fds=False, # required for posix_spawn
|
|
||||||
)
|
)
|
||||||
stdout, _ = await process.communicate()
|
stdout, _ = await process.communicate()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
@ -93,12 +88,12 @@ class CommandLineAuthProvider(AuthProvider):
|
||||||
for _line in stdout.splitlines():
|
for _line in stdout.splitlines():
|
||||||
try:
|
try:
|
||||||
line = _line.decode().lstrip()
|
line = _line.decode().lstrip()
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# malformed line
|
# malformed line
|
||||||
continue
|
continue
|
||||||
if line.startswith("#") or "=" not in line:
|
|
||||||
continue
|
|
||||||
key, _, value = line.partition("=")
|
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if key in self.ALLOWED_META_KEYS:
|
if key in self.ALLOWED_META_KEYS:
|
||||||
|
@ -122,15 +117,10 @@ class CommandLineAuthProvider(AuthProvider):
|
||||||
) -> UserMeta:
|
) -> UserMeta:
|
||||||
"""Return extra user metadata for credentials.
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
Currently, supports name, group and local_only.
|
Currently, only name is supported.
|
||||||
"""
|
"""
|
||||||
meta = self._user_meta.get(credentials.data["username"], {})
|
meta = self._user_meta.get(credentials.data["username"], {})
|
||||||
return UserMeta(
|
return UserMeta(name=meta.get("name"), is_active=True)
|
||||||
name=meta.get("name"),
|
|
||||||
is_active=True,
|
|
||||||
group=meta.get("group"),
|
|
||||||
local_only=meta.get("local_only") == "true",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandLineLoginFlow(LoginFlow):
|
class CommandLineLoginFlow(LoginFlow):
|
||||||
|
@ -138,7 +128,7 @@ class CommandLineLoginFlow(LoginFlow):
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Home Assistant auth provider."""
|
"""Home Assistant auth provider."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -13,12 +12,11 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_ID
|
from homeassistant.const import CONF_ID
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import issue_registry as ir
|
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
|
|
||||||
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = "auth_provider.homeassistant"
|
STORAGE_KEY = "auth_provider.homeassistant"
|
||||||
|
@ -55,27 +53,6 @@ class InvalidUser(HomeAssistantError):
|
||||||
Will not be raised when validating authentication.
|
Will not be raised when validating authentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args: object,
|
|
||||||
translation_key: str | None = None,
|
|
||||||
translation_placeholders: dict[str, str] | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize exception."""
|
|
||||||
super().__init__(
|
|
||||||
*args,
|
|
||||||
translation_domain="auth",
|
|
||||||
translation_key=translation_key,
|
|
||||||
translation_placeholders=translation_placeholders,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidUsername(InvalidUser):
|
|
||||||
"""Raised when invalid username is specified.
|
|
||||||
|
|
||||||
Will not be raised when validating authentication.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
"""Hold the user data."""
|
"""Hold the user data."""
|
||||||
|
@ -83,21 +60,19 @@ class Data:
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._store = Store[dict[str, list[dict[str, str]]]](
|
self._store = hass.helpers.storage.Store(
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||||
)
|
)
|
||||||
self._data: dict[str, list[dict[str, str]]] | None = None
|
self._data: dict[str, Any] | None = None
|
||||||
# Legacy mode will allow usernames to start/end with whitespace
|
# Legacy mode will allow usernames to start/end with whitespace
|
||||||
# and will compare usernames case-insensitive.
|
# and will compare usernames case-insensitive.
|
||||||
# Deprecated in June 2019 and will be removed in 2026.7
|
# Remove in 2020 or when we launch 1.0.
|
||||||
self.is_legacy = False
|
self.is_legacy = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def normalize_username(
|
def normalize_username(self, username: str) -> str:
|
||||||
self, username: str, *, force_normalize: bool = False
|
|
||||||
) -> str:
|
|
||||||
"""Normalize a username based on the mode."""
|
"""Normalize a username based on the mode."""
|
||||||
if self.is_legacy and not force_normalize:
|
if self.is_legacy:
|
||||||
return username
|
return username
|
||||||
|
|
||||||
return username.strip().casefold()
|
return username.strip().casefold()
|
||||||
|
@ -105,57 +80,47 @@ class Data:
|
||||||
async def async_load(self) -> None:
|
async def async_load(self) -> None:
|
||||||
"""Load stored data."""
|
"""Load stored data."""
|
||||||
if (data := await self._store.async_load()) is None:
|
if (data := await self._store.async_load()) is None:
|
||||||
data = cast(dict[str, list[dict[str, str]]], {"users": []})
|
data = {"users": []}
|
||||||
|
|
||||||
self._async_check_for_not_normalized_usernames(data)
|
seen: set[str] = set()
|
||||||
self._data = data
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_check_for_not_normalized_usernames(
|
|
||||||
self, data: dict[str, list[dict[str, str]]]
|
|
||||||
) -> None:
|
|
||||||
not_normalized_usernames: set[str] = set()
|
|
||||||
|
|
||||||
for user in data["users"]:
|
for user in data["users"]:
|
||||||
username = user["username"]
|
username = user["username"]
|
||||||
|
|
||||||
if self.normalize_username(username, force_normalize=True) != username:
|
# check if we have duplicates
|
||||||
|
if (folded := username.casefold()) in seen:
|
||||||
|
self.is_legacy = True
|
||||||
|
|
||||||
logging.getLogger(__name__).warning(
|
logging.getLogger(__name__).warning(
|
||||||
(
|
|
||||||
"Home Assistant auth provider is running in legacy mode "
|
"Home Assistant auth provider is running in legacy mode "
|
||||||
"because we detected usernames that are normalized (lowercase and without spaces)."
|
"because we detected usernames that are case-insensitive"
|
||||||
" Please change the username: '%s'."
|
"equivalent. Please change the username: '%s'.",
|
||||||
),
|
|
||||||
username,
|
username,
|
||||||
)
|
)
|
||||||
not_normalized_usernames.add(username)
|
|
||||||
|
|
||||||
if not_normalized_usernames:
|
break
|
||||||
|
|
||||||
|
seen.add(folded)
|
||||||
|
|
||||||
|
# check if we have unstripped usernames
|
||||||
|
if username != username.strip():
|
||||||
self.is_legacy = True
|
self.is_legacy = True
|
||||||
ir.async_create_issue(
|
|
||||||
self.hass,
|
logging.getLogger(__name__).warning(
|
||||||
"auth",
|
"Home Assistant auth provider is running in legacy mode "
|
||||||
"homeassistant_provider_not_normalized_usernames",
|
"because we detected usernames that start or end in a "
|
||||||
breaks_in_ha_version="2026.7.0",
|
"space. Please change the username: '%s'.",
|
||||||
is_fixable=False,
|
username,
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="homeassistant_provider_not_normalized_usernames",
|
|
||||||
translation_placeholders={
|
|
||||||
"usernames": f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"'
|
|
||||||
},
|
|
||||||
learn_more_url="homeassistant://config/users",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.is_legacy = False
|
|
||||||
ir.async_delete_issue(
|
|
||||||
self.hass, "auth", "homeassistant_provider_not_normalized_usernames"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
self._data = data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self) -> list[dict[str, str]]:
|
def users(self) -> list[dict[str, str]]:
|
||||||
"""Return users."""
|
"""Return users."""
|
||||||
assert self._data is not None
|
return self._data["users"] # type: ignore
|
||||||
return self._data["users"]
|
|
||||||
|
|
||||||
def validate_login(self, username: str, password: str) -> None:
|
def validate_login(self, username: str, password: str) -> None:
|
||||||
"""Validate a username and password.
|
"""Validate a username and password.
|
||||||
|
@ -182,7 +147,9 @@ class Data:
|
||||||
if not bcrypt.checkpw(password.encode(), user_hash):
|
if not bcrypt.checkpw(password.encode(), user_hash):
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
def hash_password( # pylint: disable=no-self-use
|
||||||
|
self, password: str, for_storage: bool = False
|
||||||
|
) -> bytes:
|
||||||
"""Encode a password."""
|
"""Encode a password."""
|
||||||
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||||
|
|
||||||
|
@ -191,11 +158,13 @@ class Data:
|
||||||
return hashed
|
return hashed
|
||||||
|
|
||||||
def add_auth(self, username: str, password: str) -> None:
|
def add_auth(self, username: str, password: str) -> None:
|
||||||
"""Add a new authenticated user/pass.
|
"""Add a new authenticated user/pass."""
|
||||||
|
username = self.normalize_username(username)
|
||||||
|
|
||||||
Raises InvalidUsername if the new username is invalid.
|
if any(
|
||||||
"""
|
self.normalize_username(user["username"]) == username for user in self.users
|
||||||
self._validate_new_username(username)
|
):
|
||||||
|
raise InvalidUser
|
||||||
|
|
||||||
self.users.append(
|
self.users.append(
|
||||||
{
|
{
|
||||||
|
@ -216,7 +185,7 @@ class Data:
|
||||||
break
|
break
|
||||||
|
|
||||||
if index is None:
|
if index is None:
|
||||||
raise InvalidUser(translation_key="user_not_found")
|
raise InvalidUser
|
||||||
|
|
||||||
self.users.pop(index)
|
self.users.pop(index)
|
||||||
|
|
||||||
|
@ -232,54 +201,10 @@ class Data:
|
||||||
user["password"] = self.hash_password(new_password, True).decode()
|
user["password"] = self.hash_password(new_password, True).decode()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidUser(translation_key="user_not_found")
|
raise InvalidUser
|
||||||
|
|
||||||
@callback
|
|
||||||
def _validate_new_username(self, new_username: str) -> None:
|
|
||||||
"""Validate that username is normalized and unique.
|
|
||||||
|
|
||||||
Raises InvalidUsername if the new username is invalid.
|
|
||||||
"""
|
|
||||||
normalized_username = self.normalize_username(
|
|
||||||
new_username, force_normalize=True
|
|
||||||
)
|
|
||||||
if normalized_username != new_username:
|
|
||||||
raise InvalidUsername(
|
|
||||||
translation_key="username_not_normalized",
|
|
||||||
translation_placeholders={"new_username": new_username},
|
|
||||||
)
|
|
||||||
|
|
||||||
if any(
|
|
||||||
self.normalize_username(user["username"]) == normalized_username
|
|
||||||
for user in self.users
|
|
||||||
):
|
|
||||||
raise InvalidUsername(
|
|
||||||
translation_key="username_already_exists",
|
|
||||||
translation_placeholders={"username": new_username},
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def change_username(self, username: str, new_username: str) -> None:
|
|
||||||
"""Update the username.
|
|
||||||
|
|
||||||
Raises InvalidUser if user cannot be found.
|
|
||||||
Raises InvalidUsername if the new username is invalid.
|
|
||||||
"""
|
|
||||||
username = self.normalize_username(username)
|
|
||||||
self._validate_new_username(new_username)
|
|
||||||
|
|
||||||
for user in self.users:
|
|
||||||
if self.normalize_username(user["username"]) == username:
|
|
||||||
user["username"] = new_username
|
|
||||||
assert self._data is not None
|
|
||||||
self._async_check_for_not_normalized_usernames(self._data)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise InvalidUser(translation_key="user_not_found")
|
|
||||||
|
|
||||||
async def async_save(self) -> None:
|
async def async_save(self) -> None:
|
||||||
"""Save data."""
|
"""Save data."""
|
||||||
if self._data is not None:
|
|
||||||
await self._store.async_save(self._data)
|
await self._store.async_save(self._data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -305,7 +230,7 @@ class HassAuthProvider(AuthProvider):
|
||||||
await data.async_load()
|
await data.async_load()
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return HassLoginFlow(self)
|
return HassLoginFlow(self)
|
||||||
|
|
||||||
|
@ -348,20 +273,6 @@ class HassAuthProvider(AuthProvider):
|
||||||
)
|
)
|
||||||
await self.data.async_save()
|
await self.data.async_save()
|
||||||
|
|
||||||
async def async_change_username(
|
|
||||||
self, credential: Credentials, new_username: str
|
|
||||||
) -> None:
|
|
||||||
"""Validate new username and change it including updating credentials object."""
|
|
||||||
if self.data is None:
|
|
||||||
await self.async_initialize()
|
|
||||||
assert self.data is not None
|
|
||||||
|
|
||||||
self.data.change_username(credential.data["username"], new_username)
|
|
||||||
self.hass.auth.async_update_user_credentials_data(
|
|
||||||
credential, {**credential.data, "username": new_username}
|
|
||||||
)
|
|
||||||
await self.data.async_save()
|
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Mapping[str, str]
|
self, flow_result: Mapping[str, str]
|
||||||
) -> Credentials:
|
) -> Credentials:
|
||||||
|
@ -405,7 +316,7 @@ class HassLoginFlow(LoginFlow):
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
"""Example auth provider."""
|
"""Example auth provider."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import hmac
|
import hmac
|
||||||
from typing import cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema(
|
USER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -36,7 +36,7 @@ class InvalidAuthError(HomeAssistantError):
|
||||||
class ExampleAuthProvider(AuthProvider):
|
class ExampleAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return ExampleLoginFlow(self)
|
return ExampleLoginFlow(self)
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class ExampleLoginFlow(LoginFlow):
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = None
|
errors = None
|
||||||
|
|
||||||
|
|
106
homeassistant/auth/providers/legacy_api_password.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
Support Legacy API password auth provider.
|
||||||
|
|
||||||
|
It will be removed when auth system production ready
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
import hmac
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
|
AUTH_PROVIDER_TYPE = "legacy_api_password"
|
||||||
|
CONF_API_PASSWORD = "api_password"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||||
|
{vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA
|
||||||
|
)
|
||||||
|
|
||||||
|
LEGACY_USER_NAME = "Legacy API password user"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuthError(HomeAssistantError):
|
||||||
|
"""Raised when submitting invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
|
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
|
||||||
|
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||||
|
"""An auth provider support legacy api_password."""
|
||||||
|
|
||||||
|
DEFAULT_TITLE = "Legacy API Password"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_password(self) -> str:
|
||||||
|
"""Return api_password."""
|
||||||
|
return str(self.config[CONF_API_PASSWORD])
|
||||||
|
|
||||||
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
|
"""Return a flow to login."""
|
||||||
|
return LegacyLoginFlow(self)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_validate_login(self, password: str) -> None:
|
||||||
|
"""Validate password."""
|
||||||
|
api_password = str(self.config[CONF_API_PASSWORD])
|
||||||
|
|
||||||
|
if not hmac.compare_digest(
|
||||||
|
api_password.encode("utf-8"), password.encode("utf-8")
|
||||||
|
):
|
||||||
|
raise InvalidAuthError
|
||||||
|
|
||||||
|
async def async_get_or_create_credentials(
|
||||||
|
self, flow_result: Mapping[str, str]
|
||||||
|
) -> Credentials:
|
||||||
|
"""Return credentials for this login."""
|
||||||
|
credentials = await self.async_credentials()
|
||||||
|
if credentials:
|
||||||
|
return credentials[0]
|
||||||
|
|
||||||
|
return self.async_create_credentials({})
|
||||||
|
|
||||||
|
async def async_user_meta_for_credentials(
|
||||||
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
|
"""
|
||||||
|
Return info for the user.
|
||||||
|
|
||||||
|
Will be used to populate info when creating a new user.
|
||||||
|
"""
|
||||||
|
return UserMeta(name=LEGACY_USER_NAME, is_active=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyLoginFlow(LoginFlow):
|
||||||
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the step of the form."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
cast(
|
||||||
|
LegacyApiPasswordAuthProvider, self._auth_provider
|
||||||
|
).async_validate_login(user_input["password"])
|
||||||
|
except InvalidAuthError:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return await self.async_finish({})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema({vol.Required("password"): str}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
|
@ -3,7 +3,6 @@
|
||||||
It shows list of users if access from trusted network.
|
It shows list of users if access from trusted network.
|
||||||
Abort login flow if not access from trusted network.
|
Abort login flow if not access from trusted network.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
@ -15,27 +14,21 @@ from ipaddress import (
|
||||||
ip_address,
|
ip_address,
|
||||||
ip_network,
|
ip_network,
|
||||||
)
|
)
|
||||||
from typing import Any, cast
|
from typing import Any, Union, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.network import is_cloud_connection
|
|
||||||
|
|
||||||
from .. import InvalidAuthError
|
|
||||||
from ..models import (
|
|
||||||
AuthFlowContext,
|
|
||||||
AuthFlowResult,
|
|
||||||
Credentials,
|
|
||||||
RefreshToken,
|
|
||||||
UserMeta,
|
|
||||||
)
|
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
from .. import InvalidAuthError
|
||||||
|
from ..models import Credentials, RefreshToken, UserMeta
|
||||||
|
|
||||||
type IPAddress = IPv4Address | IPv6Address
|
IPAddress = Union[IPv4Address, IPv6Address]
|
||||||
type IPNetwork = IPv4Network | IPv6Network
|
IPNetwork = Union[IPv4Network, IPv6Network]
|
||||||
|
|
||||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||||
CONF_TRUSTED_USERS = "trusted_users"
|
CONF_TRUSTED_USERS = "trusted_users"
|
||||||
|
@ -53,7 +46,7 @@ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||||
[
|
[
|
||||||
vol.Or(
|
vol.Or(
|
||||||
cv.uuid4_hex,
|
cv.uuid4_hex,
|
||||||
vol.Schema({vol.Required(CONF_GROUP): str}),
|
vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -104,7 +97,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
"""Trusted Networks auth provider does not support MFA."""
|
"""Trusted Networks auth provider does not support MFA."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
assert context is not None
|
assert context is not None
|
||||||
ip_addr = cast(IPAddress, context.get("ip_address"))
|
ip_addr = cast(IPAddress, context.get("ip_address"))
|
||||||
|
@ -199,7 +192,10 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies):
|
if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies):
|
||||||
raise InvalidAuthError("Can't allow access from a proxy server")
|
raise InvalidAuthError("Can't allow access from a proxy server")
|
||||||
|
|
||||||
if is_cloud_connection(self.hass):
|
if "cloud" in self.hass.config.components:
|
||||||
|
from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
|
if remote.is_cloud_request.get():
|
||||||
raise InvalidAuthError("Can't allow access from Home Assistant Cloud")
|
raise InvalidAuthError("Can't allow access from Home Assistant Cloud")
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -232,7 +228,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
) -> AuthFlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
try:
|
try:
|
||||||
cast(
|
cast(
|
||||||
|
|
|
@ -1,279 +0,0 @@
|
||||||
A. HISTORY OF THE SOFTWARE
|
|
||||||
==========================
|
|
||||||
|
|
||||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
|
||||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
|
||||||
as a successor of a language called ABC. Guido remains Python's
|
|
||||||
principal author, although it includes many contributions from others.
|
|
||||||
|
|
||||||
In 1995, Guido continued his work on Python at the Corporation for
|
|
||||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
|
||||||
in Reston, Virginia where he released several versions of the
|
|
||||||
software.
|
|
||||||
|
|
||||||
In May 2000, Guido and the Python core development team moved to
|
|
||||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
|
||||||
year, the PythonLabs team moved to Digital Creations, which became
|
|
||||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
|
||||||
https://www.python.org/psf/) was formed, a non-profit organization
|
|
||||||
created specifically to own Python-related Intellectual Property.
|
|
||||||
Zope Corporation was a sponsoring member of the PSF.
|
|
||||||
|
|
||||||
All Python releases are Open Source (see https://opensource.org for
|
|
||||||
the Open Source Definition). Historically, most, but not all, Python
|
|
||||||
releases have also been GPL-compatible; the table below summarizes
|
|
||||||
the various releases.
|
|
||||||
|
|
||||||
Release Derived Year Owner GPL-
|
|
||||||
from compatible? (1)
|
|
||||||
|
|
||||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
|
||||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
|
||||||
1.6 1.5.2 2000 CNRI no
|
|
||||||
2.0 1.6 2000 BeOpen.com no
|
|
||||||
1.6.1 1.6 2001 CNRI yes (2)
|
|
||||||
2.1 2.0+1.6.1 2001 PSF no
|
|
||||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
|
||||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
|
||||||
2.1.2 2.1.1 2002 PSF yes
|
|
||||||
2.1.3 2.1.2 2002 PSF yes
|
|
||||||
2.2 and above 2.1.1 2001-now PSF yes
|
|
||||||
|
|
||||||
Footnotes:
|
|
||||||
|
|
||||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
|
||||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
|
||||||
a modified version without making your changes open source. The
|
|
||||||
GPL-compatible licenses make it possible to combine Python with
|
|
||||||
other software that is released under the GPL; the others don't.
|
|
||||||
|
|
||||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
|
||||||
because its license has a choice of law clause. According to
|
|
||||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
|
||||||
is "not incompatible" with the GPL.
|
|
||||||
|
|
||||||
Thanks to the many outside volunteers who have worked under Guido's
|
|
||||||
direction to make these releases possible.
|
|
||||||
|
|
||||||
|
|
||||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
|
||||||
===============================================================
|
|
||||||
|
|
||||||
Python software and documentation are licensed under the
|
|
||||||
Python Software Foundation License Version 2.
|
|
||||||
|
|
||||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
|
||||||
the documentation are dual licensed under the PSF License Version 2
|
|
||||||
and the Zero-Clause BSD license.
|
|
||||||
|
|
||||||
Some software incorporated into Python is under different licenses.
|
|
||||||
The licenses are listed with code falling under that license.
|
|
||||||
|
|
||||||
|
|
||||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
|
||||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
|
||||||
otherwise using this software ("Python") in source or binary form and
|
|
||||||
its associated documentation.
|
|
||||||
|
|
||||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
|
||||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
|
||||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
|
||||||
distribute, and otherwise use Python alone or in any derivative version,
|
|
||||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
|
||||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
|
||||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
|
||||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
|
||||||
prepared by Licensee.
|
|
||||||
|
|
||||||
3. In the event Licensee prepares a derivative work that is based on
|
|
||||||
or incorporates Python or any part thereof, and wants to make
|
|
||||||
the derivative work available to others as provided herein, then
|
|
||||||
Licensee hereby agrees to include in any such work a brief summary of
|
|
||||||
the changes made to Python.
|
|
||||||
|
|
||||||
4. PSF is making Python available to Licensee on an "AS IS"
|
|
||||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
|
||||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
|
||||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
|
||||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
|
||||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
|
||||||
|
|
||||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
|
||||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
|
||||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
|
||||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
|
||||||
|
|
||||||
6. This License Agreement will automatically terminate upon a material
|
|
||||||
breach of its terms and conditions.
|
|
||||||
|
|
||||||
7. Nothing in this License Agreement shall be deemed to create any
|
|
||||||
relationship of agency, partnership, or joint venture between PSF and
|
|
||||||
Licensee. This License Agreement does not grant permission to use PSF
|
|
||||||
trademarks or trade name in a trademark sense to endorse or promote
|
|
||||||
products or services of Licensee, or any third party.
|
|
||||||
|
|
||||||
8. By copying, installing or otherwise using Python, Licensee
|
|
||||||
agrees to be bound by the terms and conditions of this License
|
|
||||||
Agreement.
|
|
||||||
|
|
||||||
|
|
||||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
|
||||||
|
|
||||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
|
||||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
|
||||||
Individual or Organization ("Licensee") accessing and otherwise using
|
|
||||||
this software in source or binary form and its associated
|
|
||||||
documentation ("the Software").
|
|
||||||
|
|
||||||
2. Subject to the terms and conditions of this BeOpen Python License
|
|
||||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
|
||||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
|
||||||
and/or display publicly, prepare derivative works, distribute, and
|
|
||||||
otherwise use the Software alone or in any derivative version,
|
|
||||||
provided, however, that the BeOpen Python License is retained in the
|
|
||||||
Software, alone or in any derivative version prepared by Licensee.
|
|
||||||
|
|
||||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
|
||||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
|
||||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
|
||||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
|
||||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
|
||||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
|
||||||
|
|
||||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
|
||||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
|
||||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
|
||||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
|
||||||
|
|
||||||
5. This License Agreement will automatically terminate upon a material
|
|
||||||
breach of its terms and conditions.
|
|
||||||
|
|
||||||
6. This License Agreement shall be governed by and interpreted in all
|
|
||||||
respects by the law of the State of California, excluding conflict of
|
|
||||||
law provisions. Nothing in this License Agreement shall be deemed to
|
|
||||||
create any relationship of agency, partnership, or joint venture
|
|
||||||
between BeOpen and Licensee. This License Agreement does not grant
|
|
||||||
permission to use BeOpen trademarks or trade names in a trademark
|
|
||||||
sense to endorse or promote products or services of Licensee, or any
|
|
||||||
third party. As an exception, the "BeOpen Python" logos available at
|
|
||||||
http://www.pythonlabs.com/logos.html may be used according to the
|
|
||||||
permissions granted on that web page.
|
|
||||||
|
|
||||||
7. By copying, installing or otherwise using the software, Licensee
|
|
||||||
agrees to be bound by the terms and conditions of this License
|
|
||||||
Agreement.
|
|
||||||
|
|
||||||
|
|
||||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
|
||||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
|
||||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
|
||||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
|
||||||
source or binary form and its associated documentation.
|
|
||||||
|
|
||||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
|
||||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
|
||||||
license to reproduce, analyze, test, perform and/or display publicly,
|
|
||||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
|
||||||
alone or in any derivative version, provided, however, that CNRI's
|
|
||||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
|
||||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
|
||||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
|
||||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
|
||||||
Agreement, Licensee may substitute the following text (omitting the
|
|
||||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
|
||||||
conditions in CNRI's License Agreement. This Agreement together with
|
|
||||||
Python 1.6.1 may be located on the internet using the following
|
|
||||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
|
||||||
Agreement may also be obtained from a proxy server on the internet
|
|
||||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
|
||||||
|
|
||||||
3. In the event Licensee prepares a derivative work that is based on
|
|
||||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
|
||||||
the derivative work available to others as provided herein, then
|
|
||||||
Licensee hereby agrees to include in any such work a brief summary of
|
|
||||||
the changes made to Python 1.6.1.
|
|
||||||
|
|
||||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
|
||||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
|
||||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
|
||||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
|
||||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
|
||||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
|
||||||
|
|
||||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
|
||||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
|
||||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
|
||||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
|
||||||
|
|
||||||
6. This License Agreement will automatically terminate upon a material
|
|
||||||
breach of its terms and conditions.
|
|
||||||
|
|
||||||
7. This License Agreement shall be governed by the federal
|
|
||||||
intellectual property law of the United States, including without
|
|
||||||
limitation the federal copyright law, and, to the extent such
|
|
||||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
|
||||||
Virginia, excluding Virginia's conflict of law provisions.
|
|
||||||
Notwithstanding the foregoing, with regard to derivative works based
|
|
||||||
on Python 1.6.1 that incorporate non-separable material that was
|
|
||||||
previously distributed under the GNU General Public License (GPL), the
|
|
||||||
law of the Commonwealth of Virginia shall govern this License
|
|
||||||
Agreement only as to issues arising under or with respect to
|
|
||||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
|
||||||
License Agreement shall be deemed to create any relationship of
|
|
||||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
|
||||||
License Agreement does not grant permission to use CNRI trademarks or
|
|
||||||
trade name in a trademark sense to endorse or promote products or
|
|
||||||
services of Licensee, or any third party.
|
|
||||||
|
|
||||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
|
||||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
|
||||||
bound by the terms and conditions of this License Agreement.
|
|
||||||
|
|
||||||
ACCEPT
|
|
||||||
|
|
||||||
|
|
||||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
|
||||||
--------------------------------------------------
|
|
||||||
|
|
||||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
|
||||||
The Netherlands. All rights reserved.
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software and its
|
|
||||||
documentation for any purpose and without fee is hereby granted,
|
|
||||||
provided that the above copyright notice appear in all copies and that
|
|
||||||
both that copyright notice and this permission notice appear in
|
|
||||||
supporting documentation, and that the name of Stichting Mathematisch
|
|
||||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
|
||||||
distribution of the software without specific, written prior
|
|
||||||
permission.
|
|
||||||
|
|
||||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
|
||||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
||||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
|
||||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
||||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
PERFORMANCE OF THIS SOFTWARE.
|
|
|
@ -1,5 +0,0 @@
|
||||||
This package contains backports of Python functionality from future Python
|
|
||||||
versions.
|
|
||||||
|
|
||||||
Some of the backports have been copied directly from the CPython project,
|
|
||||||
and are subject to license agreement as detailed in LICENSE.Python.
|
|
|
@ -1,29 +1,33 @@
|
||||||
"""Enum backports from standard lib.
|
"""Enum backports from standard lib."""
|
||||||
|
|
||||||
This file contained the backport of the StrEnum of Python 3.11.
|
|
||||||
|
|
||||||
Since we have dropped support for Python 3.10, we can remove this backport.
|
|
||||||
This file is kept for now to avoid breaking custom components that might
|
|
||||||
import it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import StrEnum as _StrEnum
|
from enum import Enum
|
||||||
from functools import partial
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
from homeassistant.helpers.deprecation import (
|
T = TypeVar("T", bound="StrEnum")
|
||||||
DeprecatedAlias,
|
|
||||||
all_with_deprecated_constants,
|
|
||||||
check_if_deprecated_constant,
|
|
||||||
dir_with_deprecated_constants,
|
|
||||||
)
|
|
||||||
|
|
||||||
# StrEnum deprecated as of 2024.5 use enum.StrEnum instead.
|
|
||||||
_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5")
|
|
||||||
|
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
class StrEnum(str, Enum):
|
||||||
__dir__ = partial(
|
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
|
||||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
|
||||||
)
|
def __new__(cls: type[T], value: str, *args: Any, **kwargs: Any) -> T:
|
||||||
__all__ = all_with_deprecated_constants(globals())
|
"""Create a new StrEnum instance."""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(f"{value!r} is not a string")
|
||||||
|
return super().__new__(cls, value, *args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return self.value."""
|
||||||
|
return str(self.value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_next_value_( # pylint: disable=arguments-differ # https://github.com/PyCQA/pylint/issues/5371
|
||||||
|
name: str, start: int, count: int, last_values: list[Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Make `auto()` explicitly unsupported.
|
||||||
|
|
||||||
|
We may revisit this when it's very clear that Python 3.11's
|
||||||
|
`StrEnum.auto()` behavior will no longer change.
|
||||||
|
"""
|
||||||
|
raise TypeError("auto() is not supported by this implementation")
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
"""Functools backports from standard lib.
|
|
||||||
|
|
||||||
This file contained the backport of the cached_property implementation of Python 3.12.
|
|
||||||
|
|
||||||
Since we have dropped support for Python 3.11, we can remove this backport.
|
|
||||||
This file is kept for now to avoid breaking custom components that might
|
|
||||||
import it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
# pylint: disable-next=hass-deprecated-import
|
|
||||||
from functools import cached_property as _cached_property, partial
|
|
||||||
|
|
||||||
from homeassistant.helpers.deprecation import (
|
|
||||||
DeprecatedAlias,
|
|
||||||
all_with_deprecated_constants,
|
|
||||||
check_if_deprecated_constant,
|
|
||||||
dir_with_deprecated_constants,
|
|
||||||
)
|
|
||||||
|
|
||||||
# cached_property deprecated as of 2024.5 use functools.cached_property instead.
|
|
||||||
_DEPRECATED_cached_property = DeprecatedAlias(
|
|
||||||
_cached_property, "functools.cached_property", "2025.5"
|
|
||||||
)
|
|
||||||
|
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
|
||||||
__dir__ = partial(
|
|
||||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
|
||||||
)
|
|
||||||
__all__ = all_with_deprecated_constants(globals())
|
|
|
@ -1,126 +0,0 @@
|
||||||
"""Home Assistant module to handle restoring backups."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
|
||||||
import securetar
|
|
||||||
|
|
||||||
from .const import __version__ as HA_VERSION
|
|
||||||
|
|
||||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
|
||||||
KEEP_PATHS = ("backups",)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RestoreBackupFileContent:
|
|
||||||
"""Definition for restore backup file content."""
|
|
||||||
|
|
||||||
backup_file_path: Path
|
|
||||||
|
|
||||||
|
|
||||||
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
|
||||||
"""Return the contents of the restore backup file."""
|
|
||||||
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
|
||||||
try:
|
|
||||||
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
|
|
||||||
return RestoreBackupFileContent(
|
|
||||||
backup_file_path=Path(instruction_content["path"])
|
|
||||||
)
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _clear_configuration_directory(config_dir: Path) -> None:
|
|
||||||
"""Delete all files and directories in the config directory except for the backups directory."""
|
|
||||||
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
|
||||||
config_contents = sorted(
|
|
||||||
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
|
||||||
)
|
|
||||||
|
|
||||||
for entry in config_contents:
|
|
||||||
entrypath = config_dir.joinpath(entry)
|
|
||||||
|
|
||||||
if entrypath.is_file():
|
|
||||||
entrypath.unlink()
|
|
||||||
elif entrypath.is_dir():
|
|
||||||
shutil.rmtree(entrypath)
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
|
||||||
"""Extract the backup file to the config directory."""
|
|
||||||
with (
|
|
||||||
TemporaryDirectory() as tempdir,
|
|
||||||
securetar.SecureTarFile(
|
|
||||||
backup_file_path,
|
|
||||||
gzip=False,
|
|
||||||
mode="r",
|
|
||||||
) as ostf,
|
|
||||||
):
|
|
||||||
ostf.extractall(
|
|
||||||
path=Path(tempdir, "extracted"),
|
|
||||||
members=securetar.secure_path(ostf),
|
|
||||||
filter="fully_trusted",
|
|
||||||
)
|
|
||||||
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
|
||||||
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
|
||||||
|
|
||||||
if (
|
|
||||||
backup_meta_version := AwesomeVersion(
|
|
||||||
backup_meta["homeassistant"]["version"]
|
|
||||||
)
|
|
||||||
) > HA_VERSION:
|
|
||||||
raise ValueError(
|
|
||||||
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
|
||||||
)
|
|
||||||
|
|
||||||
with securetar.SecureTarFile(
|
|
||||||
Path(
|
|
||||||
tempdir,
|
|
||||||
"extracted",
|
|
||||||
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
|
||||||
),
|
|
||||||
gzip=backup_meta["compressed"],
|
|
||||||
mode="r",
|
|
||||||
) as istf:
|
|
||||||
for member in istf.getmembers():
|
|
||||||
if member.name == "data":
|
|
||||||
continue
|
|
||||||
member.name = member.name.replace("data/", "")
|
|
||||||
_clear_configuration_directory(config_dir)
|
|
||||||
istf.extractall(
|
|
||||||
path=config_dir,
|
|
||||||
members=[
|
|
||||||
member
|
|
||||||
for member in securetar.secure_path(istf)
|
|
||||||
if member.name != "data"
|
|
||||||
],
|
|
||||||
filter="fully_trusted",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def restore_backup(config_dir_path: str) -> bool:
|
|
||||||
"""Restore the backup file if any.
|
|
||||||
|
|
||||||
Returns True if a restore backup file was found and restored, False otherwise.
|
|
||||||
"""
|
|
||||||
config_dir = Path(config_dir_path)
|
|
||||||
if not (restore_content := restore_backup_file_content(config_dir)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
|
||||||
backup_file_path = restore_content.backup_file_path
|
|
||||||
_LOGGER.info("Restoring %s", backup_file_path)
|
|
||||||
try:
|
|
||||||
_extract_backup(config_dir, backup_file_path)
|
|
||||||
except FileNotFoundError as err:
|
|
||||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
|
||||||
_LOGGER.info("Restore complete, restarting")
|
|
||||||
return True
|
|
|
@ -1,252 +1,18 @@
|
||||||
"""Block blocking calls being done in asyncio."""
|
"""Block blocking calls being done in asyncio."""
|
||||||
|
|
||||||
import builtins
|
|
||||||
from collections.abc import Callable
|
|
||||||
from contextlib import suppress
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import glob
|
|
||||||
from http.client import HTTPConnection
|
from http.client import HTTPConnection
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from ssl import SSLContext
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .helpers.frame import get_current_frame
|
from .util.async_ import protect_loop
|
||||||
from .util.loop import protect_loop
|
|
||||||
|
|
||||||
_IN_TESTS = "unittest" in sys.modules
|
|
||||||
|
|
||||||
ALLOWED_FILE_PREFIXES = ("/proc",)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|
||||||
# If the module is already imported, we can ignore it.
|
|
||||||
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
|
||||||
# If the file is in /proc we can ignore it.
|
|
||||||
args = mapped_args["args"]
|
|
||||||
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
|
|
||||||
return path.startswith(ALLOWED_FILE_PREFIXES)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|
||||||
#
|
|
||||||
# Avoid extracting the stack unless we need to since it
|
|
||||||
# will have to access the linecache which can do blocking
|
|
||||||
# I/O and we are trying to avoid blocking calls.
|
|
||||||
#
|
|
||||||
# frame[0] is us
|
|
||||||
# frame[1] is raise_for_blocking_call
|
|
||||||
# frame[2] is protected_loop_func
|
|
||||||
# frame[3] is the offender
|
|
||||||
with suppress(ValueError):
|
|
||||||
return get_current_frame(4).f_code.co_filename.endswith("pydevd.py")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class BlockingCall:
|
|
||||||
"""Class to hold information about a blocking call."""
|
|
||||||
|
|
||||||
original_func: Callable
|
|
||||||
object: object
|
|
||||||
function: str
|
|
||||||
check_allowed: Callable[[dict[str, Any]], bool] | None
|
|
||||||
strict: bool
|
|
||||||
strict_core: bool
|
|
||||||
skip_for_tests: bool
|
|
||||||
|
|
||||||
|
|
||||||
_BLOCKING_CALLS: tuple[BlockingCall, ...] = (
|
|
||||||
BlockingCall(
|
|
||||||
original_func=HTTPConnection.putrequest,
|
|
||||||
object=HTTPConnection,
|
|
||||||
function="putrequest",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=True,
|
|
||||||
strict_core=True,
|
|
||||||
skip_for_tests=False,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=time.sleep,
|
|
||||||
object=time,
|
|
||||||
function="sleep",
|
|
||||||
check_allowed=_check_sleep_call_allowed,
|
|
||||||
strict=True,
|
|
||||||
strict_core=True,
|
|
||||||
skip_for_tests=False,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=glob.glob,
|
|
||||||
object=glob,
|
|
||||||
function="glob",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=False,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=glob.iglob,
|
|
||||||
object=glob,
|
|
||||||
function="iglob",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=False,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=os.walk,
|
|
||||||
object=os,
|
|
||||||
function="walk",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=False,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=os.listdir,
|
|
||||||
object=os,
|
|
||||||
function="listdir",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=os.scandir,
|
|
||||||
object=os,
|
|
||||||
function="scandir",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=builtins.open,
|
|
||||||
object=builtins,
|
|
||||||
function="open",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=importlib.import_module,
|
|
||||||
object=importlib,
|
|
||||||
function="import_module",
|
|
||||||
check_allowed=_check_import_call_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=SSLContext.load_default_certs,
|
|
||||||
object=SSLContext,
|
|
||||||
function="load_default_certs",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=SSLContext.load_verify_locations,
|
|
||||||
object=SSLContext,
|
|
||||||
function="load_verify_locations",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=SSLContext.load_cert_chain,
|
|
||||||
object=SSLContext,
|
|
||||||
function="load_cert_chain",
|
|
||||||
check_allowed=None,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=Path.open,
|
|
||||||
object=Path,
|
|
||||||
function="open",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=Path.read_text,
|
|
||||||
object=Path,
|
|
||||||
function="read_text",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=Path.read_bytes,
|
|
||||||
object=Path,
|
|
||||||
function="read_bytes",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=Path.write_text,
|
|
||||||
object=Path,
|
|
||||||
function="write_text",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
BlockingCall(
|
|
||||||
original_func=Path.write_bytes,
|
|
||||||
object=Path,
|
|
||||||
function="write_bytes",
|
|
||||||
check_allowed=_check_file_allowed,
|
|
||||||
strict=False,
|
|
||||||
strict_core=False,
|
|
||||||
skip_for_tests=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class BlockedCalls:
|
|
||||||
"""Class to track which calls are blocked."""
|
|
||||||
|
|
||||||
calls: set[BlockingCall]
|
|
||||||
|
|
||||||
|
|
||||||
_BLOCKED_CALLS = BlockedCalls(set())
|
|
||||||
|
|
||||||
|
|
||||||
def enable() -> None:
|
def enable() -> None:
|
||||||
"""Enable the detection of blocking calls in the event loop."""
|
"""Enable the detection of blocking calls in the event loop."""
|
||||||
calls = _BLOCKED_CALLS.calls
|
# Prevent urllib3 and requests doing I/O in event loop
|
||||||
if calls:
|
HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore
|
||||||
raise RuntimeError("Blocking call detection is already enabled")
|
|
||||||
|
|
||||||
loop_thread_id = threading.get_ident()
|
# Prevent sleeping in event loop. Non-strict since 2022.02
|
||||||
for blocking_call in _BLOCKING_CALLS:
|
time.sleep = protect_loop(time.sleep, strict=False)
|
||||||
if _IN_TESTS and blocking_call.skip_for_tests:
|
|
||||||
continue
|
|
||||||
|
|
||||||
protected_function = protect_loop(
|
# Currently disabled. pytz doing I/O when getting timezone.
|
||||||
blocking_call.original_func,
|
# Prevent files being opened inside the event loop
|
||||||
strict=blocking_call.strict,
|
# builtins.open = protect_loop(builtins.open)
|
||||||
strict_core=blocking_call.strict_core,
|
|
||||||
check_allowed=blocking_call.check_allowed,
|
|
||||||
loop_thread_id=loop_thread_id,
|
|
||||||
)
|
|
||||||
setattr(blocking_call.object, blocking_call.function, protected_function)
|
|
||||||
calls.add(blocking_call)
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "airthings",
|
|
||||||
"name": "Airthings",
|
|
||||||
"integrations": ["airthings", "airthings_ble"]
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "airvisual",
|
|
||||||
"name": "AirVisual",
|
|
||||||
"integrations": ["airvisual", "airvisual_pro"]
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "airzone",
|
|
||||||
"name": "Airzone",
|
|
||||||
"integrations": ["airzone", "airzone_cloud"]
|
|
||||||
}
|
|