mirror of
https://github.com/home-assistant/developers.home-assistant.git
synced 2025-06-18 16:16:30 +00:00
Add integration quality scale docs (#2457)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Franck Nijhof <frenck@frenck.nl> Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com>
This commit is contained in:
parent
a1f286224a
commit
cf52c30bc0
@ -308,7 +308,7 @@ Ensuring that the `unique_id` is unchanged should be done using `await self.asyn
|
|||||||
|
|
||||||
## Reauthentication
|
## Reauthentication
|
||||||
|
|
||||||
Gracefully handling authentication errors such as invalid, expired, or revoked tokens is needed to advance on the [Integration Quality Scale](integration_quality_scale_index.md). This example of how to add reauth to the OAuth flow created by `script.scaffold` following the pattern in [Building a Python library](api_lib_auth.md#oauth2).
|
Gracefully handling authentication errors such as invalid, expired, or revoked tokens is needed to advance on the [Integration Quality Scale](core/integration-quality-scale). This example of how to add reauth to the OAuth flow created by `script.scaffold` following the pattern in [Building a Python library](api_lib_auth.md#oauth2).
|
||||||
If you are looking for how to trigger the reauthentication flow, see [handling expired credentials](integration_setup_failures.md#handling-expired-credentials).
|
If you are looking for how to trigger the reauthentication flow, see [handling expired credentials](integration_setup_failures.md#handling-expired-credentials).
|
||||||
|
|
||||||
This example catches an authentication exception in config entry setup in `__init__.py` and instructs the user to visit the integrations page in order to reconfigure the integration.
|
This example catches an authentication exception in config entry setup in `__init__.py` and instructs the user to visit the integrations page in order to reconfigure the integration.
|
||||||
|
39
docs/core/integration-quality-scale/_includes/checklist.jsx
Normal file
39
docs/core/integration-quality-scale/_includes/checklist.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {useDocsVersion} from '@docusaurus/plugin-content-docs/client';
|
||||||
|
import CodeBlock from '@theme/CodeBlock';
|
||||||
|
|
||||||
|
const tiers = require("./tiers.json")
|
||||||
|
|
||||||
|
function getRule(ruleId, docs) {
|
||||||
|
const rule = docs[`core/integration-quality-scale/rules/${ruleId.toLowerCase()}`];
|
||||||
|
const [id, text] = rule.title.split(" (");
|
||||||
|
return {id, text};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Checklist() {
|
||||||
|
const docs = useDocsVersion().docs;
|
||||||
|
const getRuleWithDocs = (ruleId) => getRule(ruleId, docs);
|
||||||
|
return (
|
||||||
|
<CodeBlock language="markdown">
|
||||||
|
{Object.keys(tiers).map((tier) => {
|
||||||
|
return (
|
||||||
|
<div key={tier}>
|
||||||
|
{`## ${tier.charAt(0).toUpperCase() + tier.slice(1)}\n`}
|
||||||
|
{tiers[tier].map((rule) => {
|
||||||
|
if (typeof rule === "string") {
|
||||||
|
const text = docs[`core/integration-quality-scale/rules/${rule}`].title;
|
||||||
|
return `- [ ] \`${rule}\` - ${text}\n`;
|
||||||
|
}
|
||||||
|
const text = docs[`core/integration-quality-scale/rules/${rule.id}`].title;
|
||||||
|
return [
|
||||||
|
`- [ ] \`${rule.id}\` - ${text}\n`,
|
||||||
|
...rule.subchecks.map(subcheck => ` - [ ] ${subcheck}\n`)
|
||||||
|
].join('');
|
||||||
|
}).join('')}
|
||||||
|
{`\n`}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {useDocsVersion} from '@docusaurus/plugin-content-docs/client';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
|
||||||
|
const tiers = require("./tiers.json")
|
||||||
|
|
||||||
|
export default function RuleOverview({tier}) {
|
||||||
|
const docs = useDocsVersion().docs;
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{tiers[tier].map((rule) => {
|
||||||
|
let id = rule;
|
||||||
|
if (typeof rule === "object") {
|
||||||
|
id = rule.id;
|
||||||
|
}
|
||||||
|
const relatedRule = docs[`core/integration-quality-scale/rules/${id}`];
|
||||||
|
return (
|
||||||
|
<li key={id}>
|
||||||
|
<Link to={`rules/${id}`}>{id}</Link> - {relatedRule.title}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
68
docs/core/integration-quality-scale/_includes/tiers.json
Normal file
68
docs/core/integration-quality-scale/_includes/tiers.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"bronze": [
|
||||||
|
{
|
||||||
|
"id": "config-flow",
|
||||||
|
"subchecks": [
|
||||||
|
"Uses `data-description` to give context to fields",
|
||||||
|
"Uses `ConfigEntry.data` and `ConfigEntry.options` correctly"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test-before-configure",
|
||||||
|
"unique-config-entry",
|
||||||
|
"config-flow-test-coverage",
|
||||||
|
"runtime-data",
|
||||||
|
"test-before-setup",
|
||||||
|
"appropriate-polling",
|
||||||
|
"entity-unique-id",
|
||||||
|
"has-entity-name",
|
||||||
|
"entity-event-setup",
|
||||||
|
"dependency-transparency",
|
||||||
|
"action-setup",
|
||||||
|
"common-modules",
|
||||||
|
"docs-high-level-description",
|
||||||
|
"docs-installation-instructions",
|
||||||
|
"docs-removal-instructions",
|
||||||
|
"docs-actions",
|
||||||
|
"brands"
|
||||||
|
],
|
||||||
|
"silver": [
|
||||||
|
"config-entry-unloading",
|
||||||
|
"log-when-unavailable",
|
||||||
|
"entity-unavailable",
|
||||||
|
"action-exceptions",
|
||||||
|
"reauthentication-flow",
|
||||||
|
"parallel-updates",
|
||||||
|
"test-coverage",
|
||||||
|
"integration-owner",
|
||||||
|
"docs-installation-parameters",
|
||||||
|
"docs-configuration-parameters"
|
||||||
|
],
|
||||||
|
"gold": [
|
||||||
|
"entity-translations",
|
||||||
|
"entity-device-class",
|
||||||
|
"devices",
|
||||||
|
"entity-category",
|
||||||
|
"entity-disabled-by-default",
|
||||||
|
"discovery",
|
||||||
|
"stale-devices",
|
||||||
|
"diagnostics",
|
||||||
|
"exception-translations",
|
||||||
|
"icon-translations",
|
||||||
|
"reconfiguration-flow",
|
||||||
|
"dynamic-devices",
|
||||||
|
"discovery-update-info",
|
||||||
|
"repair-issues",
|
||||||
|
"docs-use-cases",
|
||||||
|
"docs-supported-devices",
|
||||||
|
"docs-supported-functions",
|
||||||
|
"docs-data-update",
|
||||||
|
"docs-known-limitations",
|
||||||
|
"docs-troubleshooting",
|
||||||
|
"docs-examples"
|
||||||
|
],
|
||||||
|
"platinum": [
|
||||||
|
"async-dependency",
|
||||||
|
"inject-websession",
|
||||||
|
"strict-typing"
|
||||||
|
]
|
||||||
|
}
|
10
docs/core/integration-quality-scale/checklist.md
Normal file
10
docs/core/integration-quality-scale/checklist.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Checklist"
|
||||||
|
---
|
||||||
|
import Checklist from './_includes/checklist.jsx'
|
||||||
|
|
||||||
|
When changing the quality scale of an integration, make sure you have completed the rules before opening the PR to change the quality scale.
|
||||||
|
In the PR description, please deliver a copy of this checklist and mark the rules that have been completed.
|
||||||
|
Make sure you add links to the relevant code to help a speedy grading process.
|
||||||
|
|
||||||
|
<Checklist />
|
186
docs/core/integration-quality-scale/index.md
Normal file
186
docs/core/integration-quality-scale/index.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
title: "Integration quality scale"
|
||||||
|
---
|
||||||
|
import RuleOverview from './_includes/rule_overview.jsx'
|
||||||
|
|
||||||
|
The integration quality scale is a framework for Home Assistant to grade integrations based on user experience, features, code quality and developer experience.
|
||||||
|
To grade this, the project has come up with a set of tiers, which all have their own meaning.
|
||||||
|
|
||||||
|
## Scaled tiers
|
||||||
|
There are 4 scaled tiers, bronze, silver, gold, and platinum.
|
||||||
|
To reach a tier, the integration must fulfill all rules of that tier and the tiers below.
|
||||||
|
|
||||||
|
These tiers are defined as follows.
|
||||||
|
|
||||||
|
### 🥉 Bronze
|
||||||
|
The bronze tier is the baseline standard and requirement for all new integrations. It meets the minimum requirements in code quality, functionality, and user experience. It complies with the fundamental expectations and provides a reliable foundation for users to interact with their devices and services.
|
||||||
|
|
||||||
|
The documentation provides guidelines for setting up the integration directly from the Home Assistant user interface.
|
||||||
|
|
||||||
|
From a technical perspective, this integration has been reviewed to comply with all baseline standards, which we require for all new integrations, including automated tests for setting up the integration.
|
||||||
|
|
||||||
|
The bronze tier has the following characteristics:
|
||||||
|
- Can be easily set up through the UI.
|
||||||
|
- The source code adheres to basic coding standards and development guidelines.
|
||||||
|
- Automated tests that guard this integration can be configured correctly.
|
||||||
|
- Offers basic end-user documentation that is enough to get users started step-by-step easily.
|
||||||
|
|
||||||
|
### 🥈 Silver
|
||||||
|
The silver tier builds upon the “Bronze” level by improving the reliability and robustness of integrations, ensuring a solid runtime experience. It ensures an integration handles errors properly, such as when authentication to a device or service fails, handles offline devices, and other errors.
|
||||||
|
|
||||||
|
The documentation for these integrations provides information on what is available in Home Assistant when this integration is used, as well as troubleshooting information when issues occur.
|
||||||
|
|
||||||
|
This integration has one or more active code owners who help maintain it to ensure the experience on this level lasts now and in the future.
|
||||||
|
|
||||||
|
The silver tier has the following characteristics:
|
||||||
|
- Provides everything “Bronze” has.
|
||||||
|
- Provides a stable user experience under various conditions.
|
||||||
|
- Has one or more active code owners who help maintain the integration.
|
||||||
|
- Correctly and automatically recover from connection errors or offline devices, without filling log files and without unnecessary messages.
|
||||||
|
- Automatically triggers re-authentication if authentication with the device or service fails.
|
||||||
|
- Offers detailed documentation of what the integration provides and instructions for troubleshooting issues.
|
||||||
|
|
||||||
|
### 🥇 Gold
|
||||||
|
The gold standard in integration user experience, providing extensive and comprehensive support for the integrated devices & services. A gold-tier integration aims to be user-friendly, fully featured, and accessible to a wider audience.
|
||||||
|
|
||||||
|
When possible, devices are automatically discovered for an easy and seamless setup, and their firmware/software can be directly updated from Home Assistant.
|
||||||
|
|
||||||
|
All provided devices and entities are named logically and fully translatable, and they have been properly categorized and enabled for long-term statistical use.
|
||||||
|
|
||||||
|
The documentation for these integrations is extensive, and primarily aimed toward end-users and understandable by non-technical consumers. Besides providing general information on the integration, the documentation provides possible example use cases, a list of compatible devices, a list of described entities the integration provides, and extensive descriptions and usage examples of available actions provided by the integration. The use of example automations, dashboards, available Blueprints, and links to additional external resources, is highly encouraged as well.
|
||||||
|
|
||||||
|
The integration provides means for debugging issues, including downloading diagnostic information and documenting troubleshooting instructions. If needed, the integration can be reconfigured via the UI.
|
||||||
|
|
||||||
|
From a technical perspective, the integration needs to have full automated test coverage of its codebase to ensure the set integration quality is maintained now and in the future.
|
||||||
|
|
||||||
|
All integrations that have devices in the Works with Home Assistant program are at least required to have this tier.
|
||||||
|
|
||||||
|
The gold tier has the following characteristics:
|
||||||
|
- Provides everything “Silver” has.
|
||||||
|
- Has the best end-user experience an integration can offer; streamlined and intuitive.
|
||||||
|
- Can be automatically discovered, simplifying the integration setup.
|
||||||
|
- Integration can be reconfigured and adjusted.
|
||||||
|
- Supports translations.
|
||||||
|
- Extensive documentation, aimed at non-technical users.
|
||||||
|
- It supports updating the software/firmware of devices through Home Assistant when possible.
|
||||||
|
- The integration has automated tests covering the entire integration.
|
||||||
|
- Required level for integrations providing devices in the Works with Home Assistant program.
|
||||||
|
|
||||||
|
### 🏆 Platinum
|
||||||
|
Platinum is the highest tier an integration can reach, the epitome of quality within Home Assistant. It not only provides the best user experience but also achieves technical excellence by adhering to the highest standards, supreme code quality, and well-optimized performance and efficiency.
|
||||||
|
|
||||||
|
The platinum tier has the following characteristics:
|
||||||
|
- Provides everything “Gold” has.
|
||||||
|
- All source code follows all coding and Home Assistant integration standards and best practices and is fully typed with type annotations and clear code comments for better code clarity and maintenance.
|
||||||
|
- A fully asynchronous integration code base ensures efficient operation.
|
||||||
|
- Implements efficient data handling, reducing network and CPU usage.
|
||||||
|
|
||||||
|
|
||||||
|
### Keeping track of the implemented rules
|
||||||
|
Integrations that are working towards a higher tier or have a tier, must add a `quality_scale.yaml` file to their integration.
|
||||||
|
The purpose of this file is to keep track of the progress of the rules that have been implemented and to keep track of exempted rules and the reason for the exemption.
|
||||||
|
An example of this file looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
config_flow: done
|
||||||
|
docs_high_level_description:
|
||||||
|
status: exempt
|
||||||
|
comment: This integration does not connect to any device or service.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adjusting the tier of an integration
|
||||||
|
Home Assistant encourages our contributors to get their integrations to the highest possible tier, to provide an excellent coding experience for our contributors and the best experience for our users.
|
||||||
|
|
||||||
|
When an integration reaches the minimum requirements for a certain tier, a contributor can open a pull request to adjust the scale for the integration.
|
||||||
|
This request needs to be accompanied by the full checklist for each rule of scale (including all rules of lower tiers), demonstrating that it has met those requirements.
|
||||||
|
The checklist can be found [here](checklist).
|
||||||
|
|
||||||
|
Once the Home Assistant core team reviews and approves it, the integration will display the new tier as of the next major release of Home Assistant.
|
||||||
|
|
||||||
|
Besides upgrading an integration to a higher tier on the scale, it is also possible for an integration to be downgraded to a lower tier.
|
||||||
|
This can, for example, happen when there is no longer an active integration code owner.
|
||||||
|
In this specific example, the integration will be downgraded to “Bronze”, even if it otherwise fully complies with the “Platinum” tier.
|
||||||
|
|
||||||
|
### Adjustments to rules contained in each tier
|
||||||
|
The world of IoT and all technologies used by Home Assistant are changing at a fast pace; not just in terms of what Home Assistant can support or do, but also in terms of the software on which Home Assistant is built. Home Assistant is pioneering the technology in the industry at a fast pace.
|
||||||
|
|
||||||
|
This also means that new insights and newly developed and adopted best practices will occur over time, resulting in new additions and improvements to the individual integration quality scale rules.
|
||||||
|
|
||||||
|
If a tier is adjusted, all integrations in that tier need to be re-evaluated and adjusted accordingly. One exception to this is integrations that have devices that are part of the Works with Home Assistant program. Those integrations will be flagged as grandfathered into their existing tier.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
One exception to this is integrations that have devices that are part of the Works with Home Assistant program. Those integrations will be flagged as grandfathered into their existing tier.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Integration quality scale rules
|
||||||
|
The rules for each tier are defined down below and come with its own page with examples and more information.
|
||||||
|
|
||||||
|
### 🥉 Bronze
|
||||||
|
<RuleOverview tier="bronze" />
|
||||||
|
|
||||||
|
|
||||||
|
### 🥈 Silver
|
||||||
|
<RuleOverview tier="silver" />
|
||||||
|
|
||||||
|
|
||||||
|
### 🥇 Gold
|
||||||
|
<RuleOverview tier="gold" />
|
||||||
|
|
||||||
|
|
||||||
|
### 🏆 Platinum
|
||||||
|
<RuleOverview tier="platinum" />
|
||||||
|
|
||||||
|
|
||||||
|
## Special tiers
|
||||||
|
There are also 4 special tiers that are used to integration that don't have a place on the scaled tier list.
|
||||||
|
This is because they are either an internal part of core, they are not in core at all, or they don't meet the minimum requirements to be graded against the scaled tiers.
|
||||||
|
|
||||||
|
The special tiers are defined as follows.
|
||||||
|
|
||||||
|
### ❓ No score
|
||||||
|
These integrations can be set up through the Home Assistant user interface. The “No score” designation doesn’t imply that they are bad or buggy, instead, it indicates that they haven’t been assessed according to the quality scale or that they need some maintenance to reach the now-considered minimum “Bronze” standard.
|
||||||
|
|
||||||
|
The “No score” tier cannot be assigned to new integrations, as they are required to have at least a “Bronze” level when introduced. The Home Assistant project encourages the community to help update these integrations without a score to meet at least the “Bronze” level requirements.
|
||||||
|
|
||||||
|
Characteristics:
|
||||||
|
- Not yet scored or lacks sufficient information for scoring.
|
||||||
|
- Can be set up via the UI, but may need enhancements for a better experience.
|
||||||
|
- May function correctly, but hasn’t been verified against current standards.
|
||||||
|
- Documentation most often provides only basic setup steps.
|
||||||
|
|
||||||
|
### 🏠 Internal
|
||||||
|
This tier is assigned to integrations used internally by Home Assistant. These integrations provide basic components and building blocks for Home Assistant's core program or for other integrations to build on top of it.
|
||||||
|
|
||||||
|
Internal integrations are maintained by the Home Assistant project and subjected to strict architectural design procedures.
|
||||||
|
|
||||||
|
Characteristics:
|
||||||
|
- Internal, built-in building blocks of the Home Assistant core program.
|
||||||
|
- Provides building blocks for other integrations to use and build on top of.
|
||||||
|
- Maintained by the Home Assistant project.
|
||||||
|
|
||||||
|
### 💾 Legacy
|
||||||
|
Legacy integrations are older integrations that have been part of Home Assistant for many years, possibly since its inception. They can only be configured through YAML files and often lack active maintainers (code owners). These integrations might be complex to set up and do not adhere to current/modern end-user expectations in their use and features.
|
||||||
|
|
||||||
|
The Home Assistant project encourages the community to help migrate these integrations to the UI and update them to meet modern standards, making these integrations accessible to everyone.
|
||||||
|
|
||||||
|
Characteristics:
|
||||||
|
- Complex setup process; only configurable via YAML, without UI-based setup.
|
||||||
|
- May lack active code ownership and maintenance.
|
||||||
|
- Could be missing recent updates or bug fixes.
|
||||||
|
- Documentation may still be aimed at developers.
|
||||||
|
|
||||||
|
### 📦 Custom
|
||||||
|
Custom integrations are developed and distributed by the community, and offer additional functionalities and support for devices and services to Home Assistant. These integrations are not included in the official Home Assistant releases and can be installed manually or via third-party tools like HACS (Home Assistant Community Store).
|
||||||
|
|
||||||
|
The Home Assistant project does not review, security audit, maintain, or support third-party custom integrations. Users are encouraged to exercise caution and review the custom integration’s source and community feedback before installation.
|
||||||
|
|
||||||
|
Developers are encouraged and invited to contribute their custom integration to the Home Assistant project by aligning them with the integration quality scale and submitting them for inclusion.
|
||||||
|
|
||||||
|
Characteristics:
|
||||||
|
- Not included in the official Home Assistant releases.
|
||||||
|
- Manually installable or installable via community tools, like HACS.
|
||||||
|
- Maintained by individual developers or community members.
|
||||||
|
- User experience may vary widely.
|
||||||
|
- Functionality, security, and stability can vary widely.
|
||||||
|
- Documentation may be limited.
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {useDocsVersion} from "@docusaurus/plugin-content-docs/client";
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
|
||||||
|
|
||||||
|
export default function RelatedRules({relatedRules}) {
|
||||||
|
const docs = useDocsVersion().docs;
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{relatedRules.map((rule) => {
|
||||||
|
const relatedRule = docs[`core/integration-quality-scale/rules/${rule}`].title;
|
||||||
|
return (
|
||||||
|
<li key={rule}>
|
||||||
|
<Link to={rule}>{rule}</Link>: {relatedRule}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: "Service actions raise exceptions when encountering failures"
|
||||||
|
related_rules:
|
||||||
|
- exception-translations
|
||||||
|
- action-setup
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Things can go wrong when a service action is performed.
|
||||||
|
When this happens, the integration should raise an exception to indicate that something went wrong.
|
||||||
|
The exception message will be shown to the user in the UI, and can be used to help diagnose the issue.
|
||||||
|
The message will either be generated from the attached translation string or from the exception argument.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
When the problem is caused by incorrect usage (for example incorrect input or referencing something that does not exist) we should raise a `ServiceValidationError`.
|
||||||
|
When the problem is caused by an error in the service action itself (for example, a network error or a bug in the service), we should raise a `HomeAssistantError`.
|
||||||
|
|
||||||
|
In this example, we show a function that is registered as a service action in Home Assistant.
|
||||||
|
If the input is incorrect (when the end date is before the start date), a `ServiceValidationError` is raised, and if we can't reach the service, we raise a `HomeAssistantError`.
|
||||||
|
|
||||||
|
```python {6,10} showLineNumbers
|
||||||
|
async def async_set_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
|
"""Set the schedule for a day."""
|
||||||
|
start_date = call.data[ATTR_START_DATE]
|
||||||
|
end_date = call.data[ATTR_END_DATE]
|
||||||
|
if end_date < start_date:
|
||||||
|
raise ServiceValidationError("End date must be after start date")
|
||||||
|
try:
|
||||||
|
await client.set_schedule(start_date, end_date)
|
||||||
|
except MyConnectionError as err:
|
||||||
|
raise HomeAssistantError("Could not connect to the schedule") from err
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more info on raising exceptions, check the [documentation](../../platform/raising_exceptions).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
59
docs/core/integration-quality-scale/rules/action-setup.md
Normal file
59
docs/core/integration-quality-scale/rules/action-setup.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "Service actions are registered in async_setup"
|
||||||
|
related_rules:
|
||||||
|
- action-exceptions
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Integrations can add their own service actions to Home Assistant.
|
||||||
|
In the past, they have been frequently registered in the `async_setup_entry` method and removed in the `async_unload_entry` method.
|
||||||
|
The result of this is that the service actions are only available when there is a loaded entry.
|
||||||
|
This is not ideal, since this way we can't validate automations users create that use these service actions, since it is possible that the configuration entry could not be loaded.
|
||||||
|
|
||||||
|
We rather prefer integrations to set up their service actions in the `async_setup` method.
|
||||||
|
This way we can let the user know why the service action did not work, if the targeted configuration entry is not loaded.
|
||||||
|
The validation should happen inside the service action, and should raise `ServiceValidationError` if the input is invalid.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
The example below is a snippet where the service action is registered in the `async_setup` method.
|
||||||
|
In this example, the service call requires a configuration entry id as parameter.
|
||||||
|
This is used to first fetch the configuration entry, and then check if it is loaded.
|
||||||
|
If the configuration entry does not exist or the configuration entry that we found is not loaded, we raise a relevant error which is shown to the user.
|
||||||
|
|
||||||
|
`__init__py`:
|
||||||
|
```python {13-19} showLineNumbers
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up my integration."""
|
||||||
|
|
||||||
|
async def async_get_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
|
"""Get the schedule for a specific range."""
|
||||||
|
if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])):
|
||||||
|
raise ServiceValidationError("Entry not found")
|
||||||
|
if entry.state is not ConfigEntryState.LOADED:
|
||||||
|
raise ServiceValidationError("Entry not loaded")
|
||||||
|
client = cast(MyConfigEntry, entry).runtime_data
|
||||||
|
...
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GET_SCHEDULE,
|
||||||
|
async_get_schedule,
|
||||||
|
schema=SERVICE_GET_SCHEDULE_SCHEMA,
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information on how to set up service actions, see the [service documentation](../../../dev_101_services).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: "If it's a polling integration, set an appropriate polling interval"
|
||||||
|
related_rules:
|
||||||
|
- parallel-updates
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
In an ideal world, all integrations would have a push-based data interface, where the device or service would let us know when new data is available.
|
||||||
|
This would decrease the amount of requests Home Assistant would make.
|
||||||
|
|
||||||
|
However, in the real world, many devices and services are not capable of push-based communication, so we have to resort to polling.
|
||||||
|
To do this responsibly, we should set an appropriate polling interval that will serve the majority of users.
|
||||||
|
|
||||||
|
There is no real definition of what an appropriate polling interval is, as it depends on the device or service being polled.
|
||||||
|
For example, we should not poll an air quality sensor every 5 seconds, as the data will not change that often.
|
||||||
|
In those cases, more than 99% of the users will be fine with a polling interval of a minute or more.
|
||||||
|
|
||||||
|
To give another example, if we poll a cloud service for solar panel data where the data is updated every hour.
|
||||||
|
It would not make sense for us to poll every minute, as the data will not change between the polls.
|
||||||
|
|
||||||
|
For the users that do want to have more frequent updates, they can [define a custom polling interval](https://www.home-assistant.io/common-tasks/general/#defining-a-custom-polling-interval)
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
There are two ways to set the polling interval.
|
||||||
|
Which one to use depends on how the integration polls for data.
|
||||||
|
When using an update coordinator, the polling interval can be set by setting the `update_interval` parameter or attribute in the coordinator.
|
||||||
|
When using the built-in entity update method, having set the `should_poll` entity attribute to `True`, the polling interval can be set by setting the `SCAN_INTERVAL` constant in the platform module.
|
||||||
|
|
||||||
|
`coordinator.py`:
|
||||||
|
```python {10} showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`sensor.py`:
|
||||||
|
```python {1} showLineNumbers
|
||||||
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a Sensor."""
|
||||||
|
|
||||||
|
_attr_should_poll = True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about polling can be found in the [documentation](../../../integration_fetching_data).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "Dependency is async"
|
||||||
|
related_rules:
|
||||||
|
- inject-websession
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Home Assistant works with asyncio to be efficient when handling tasks.
|
||||||
|
To avoid switching context between the asyncio event loop and other threads, which is costly performance wise, ideally, your library should also use asyncio.
|
||||||
|
|
||||||
|
This results not only in a more efficient system but the code is also more neat.
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information on how to create a library can be found in the [documentation](../../../api_lib_index).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
14
docs/core/integration-quality-scale/rules/brands.md
Normal file
14
docs/core/integration-quality-scale/rules/brands.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: "Has branding assets available for the integration"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Branding assets are important for the integration to be easily recognizable and to provide a consistent look and feel across all integrations.
|
||||||
|
The project keeps track of them in the [brands repository](https://github.com/home-assistant/brands).
|
||||||
|
|
||||||
|
The requirements on the needed assets can be found in the [readme](https://github.com/home-assistant/brands/blob/master/README.md) of the brands repository.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
54
docs/core/integration-quality-scale/rules/common-modules.md
Normal file
54
docs/core/integration-quality-scale/rules/common-modules.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: "Place common patterns in common modules"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
The Home Assistant codebase has a few common patterns that have originated over time.
|
||||||
|
For example, the majority of new integrations use a coordinator to centralize their data fetching.
|
||||||
|
The coordinator should be placed in `coordinator.py`.
|
||||||
|
This increases consistency between integrations and makes it easier to find the coordinator for a specific integration.
|
||||||
|
|
||||||
|
The second common pattern is the base entity.
|
||||||
|
Since a lot of integrations provide more types of entities, a base entity can prove useful to reduce code duplication.
|
||||||
|
The base entity should be placed in `entity.py`.
|
||||||
|
|
||||||
|
The efforts done to increase consistency between integrations have a positive impact on the quality of the codebase and the developer experience.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example we have a coordinator, stored in `coordinator.py`, and a base entity, stored in `entity.py`.
|
||||||
|
|
||||||
|
`coordinator.py`
|
||||||
|
```python showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: MyClient) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
```
|
||||||
|
|
||||||
|
`entity.py`
|
||||||
|
```python showLineNumbers
|
||||||
|
class MyEntity(CoordinatorEntity[MyCoordinator]):
|
||||||
|
"""Base entity for MyIntegration."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, coordinator: MyCoordinator) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_device_infp = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "Support config entry unloading"
|
||||||
|
related_rules:
|
||||||
|
- entity-event-setup
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Integrations should support config entry unloading.
|
||||||
|
This allows Home Assistant to unload the integration on runtime, allowing the user to remove the integration or to reload it without having to restart Home Assistant.
|
||||||
|
|
||||||
|
This improves the user experience, since the user can do more actions without having to restart Home Assistant.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the `async_unload_entry` interface function, the integration should clean up any subscriptions and close any connections opened during the setup of the integration.
|
||||||
|
|
||||||
|
The method that has to be added to `__init__.py` looks very similar to the `async_setup_entry` method.
|
||||||
|
In this example we have a listener, stored in the `runtime_data` of the config entry, which we want to clean up to avoid memory leaks.
|
||||||
|
|
||||||
|
`__init__.py`:
|
||||||
|
```python showLineNumbers
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if (unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS))
|
||||||
|
entry.runtime_data.listener()
|
||||||
|
return unload_ok
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
You can also use `entry.async_on_unload` to register a callback that will be called when the config entry is unloaded.
|
||||||
|
This can be useful to clean up resources without having to keep track of the removal methods yourself.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config entries and their lifecycle can be found in the [config entry documentation](../../../config_entries_index).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: "Full test coverage for the config flow"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- test-before-configure
|
||||||
|
- unique-config-entry
|
||||||
|
- discovery
|
||||||
|
- reauthentication-flow
|
||||||
|
- reconfiguration-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
The config flow is the first interaction a user has with your integration.
|
||||||
|
It is important to ensure that the config flow is working as expected and that the user can set up the integration without any issues or (config flow related) errors.
|
||||||
|
|
||||||
|
This means that we want to have **100%** test coverage for the config flow.
|
||||||
|
In those tests, we require verification that the flow is able to recover from an error to confirm that the user is able to finish the flow even if something goes wrong.
|
||||||
|
|
||||||
|
Since we want the user to have a smooth experience using other integration flows, this rule also applies to the reconfigure, reauthentication, and options flows.
|
||||||
|
|
||||||
|
The extra added benefit of having tests for an integration is that it introduces the developer to testing, making it easier to write tests for other parts of the integration.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
Even though the code used to check the uniqueness of a config entry is most likely touched by the happy flow tests, make sure to also test that the flow doesn't allow adding more than one unique configuration entry to reach complete coverage.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
We need to test the following scenarios for each way the config flow can be initiated, either by the user, by discovery, or by an import flow.
|
||||||
|
|
||||||
|
The example below shows a basic happy flow initiated by the user.
|
||||||
|
|
||||||
|
`test_config_flow.py`:
|
||||||
|
```python showLineNumbers
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_my_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test full flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: "10.0.0.131"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "My integration"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "10.0.0.131",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config flows can be found in the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
More information about testing integrations can be found in the [testing documentation](../../../development_testing).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
81
docs/core/integration-quality-scale/rules/config-flow.md
Normal file
81
docs/core/integration-quality-scale/rules/config-flow.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: "Integration needs to be able to be set up via the UI"
|
||||||
|
related_rules:
|
||||||
|
- test-before-configure
|
||||||
|
- unique-config-entry
|
||||||
|
- config-flow-test-coverage
|
||||||
|
- discovery
|
||||||
|
- reauthentication-flow
|
||||||
|
- reconfiguration-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Since its introduction in 2018, the config flow has become the standard way to set up integrations in Home Assistant.
|
||||||
|
They allow for a consistent user experience across integrations and provide a way to guide users through the setup process.
|
||||||
|
|
||||||
|
Because of the better user experience, we want to make sure that all integrations are able to set up via the config flow.
|
||||||
|
|
||||||
|
Since this is the entrypoint for users to start using an integration, we should make sure that the config flow is very user-friendly and understandable.
|
||||||
|
This means we should use the right selectors at the right place, validate the input where needed, and use `data_description` in the `strings.json` to give context about the input field.
|
||||||
|
|
||||||
|
The integration should store all configuration in the `ConfigEntry.data` field, while all settings that are not needed for the connection to be made should be stored in the `ConfigEntry.options` field.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
To use a config flow in your integration, you need to create a `config_flow.py` file in your integration folder and set `config_flow` in your `manifest.json` to `true`.
|
||||||
|
The text that is shown in the config flow is defined in the `strings.json` file.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`string.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "The hostname or IP address of the MyIntegration device."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config flows can be found in the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
More information about the architecture decision around config flows can be found in [ADR-0010](https://github.com/home-assistant/architecture/blob/master/adr/0010-integration-configuration.md)
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
The integrations that are exempt in [ADR-0010](https://github.com/home-assistant/architecture/blob/master/adr/0010-integration-configuration.md) are exempt from this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "Dependency transparency"
|
||||||
|
related_rules:
|
||||||
|
- async-dependency
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Home Assistant uses a lot of dependencies to work.
|
||||||
|
These dependencies will be shipped with new versions of Home Assistant.
|
||||||
|
In order for the project to trust the dependencies, we have a set of requirements we want the dependencies to meet.
|
||||||
|
|
||||||
|
- The source code of the dependency must be available under an OSI-approved license.
|
||||||
|
- The dependency must be available on PyPI.
|
||||||
|
- The package published to PyPi should be built and published inside a CI pipeline.
|
||||||
|
- The version of the dependency published on PyPI should correspond to a tagged release in an open online repository.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
56
docs/core/integration-quality-scale/rules/devices.md
Normal file
56
docs/core/integration-quality-scale/rules/devices.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: "The integration creates devices"
|
||||||
|
related_rules:
|
||||||
|
- has-entity-name
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Devices, in Home Assistant, are used to group entities to represent either a single physical device or a service.
|
||||||
|
This is useful, since users usually think they add a device or a service to their system, not a single entity.
|
||||||
|
Home Assistant stores the device information in the device registry.
|
||||||
|
In order for the user to have the best experience, the information about the device should be as complete as possible.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, there is a sensor entity that defines which device it should be added to in the device registry, together with some metadata about the device.
|
||||||
|
This will provide a rich device info page, where the user can recognize the device by its name, serial number, and other fields.
|
||||||
|
|
||||||
|
`sensor.py`:
|
||||||
|
```python {8-18} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, device: MyDevice) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, device.mac)},
|
||||||
|
name=device.name,
|
||||||
|
serial_number=device.serial,
|
||||||
|
hw_version=device.rev,
|
||||||
|
sw_version=device.version,
|
||||||
|
manufacturer="My Company",
|
||||||
|
model="My Sensor",
|
||||||
|
model_id="ABC-123",
|
||||||
|
via_device={(DOMAIN, device.hub_id)},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If the device represents a service, be sure to add `entry_type=DeviceEntryType.SERVICE` to the `DeviceInfo` object to mark the device as such.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about devices can be found in the [device](../../../device_registry_index) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
38
docs/core/integration-quality-scale/rules/diagnostics.md
Normal file
38
docs/core/integration-quality-scale/rules/diagnostics.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: "Implements diagnostics"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Diagnostics are an easy way for the user to gather data about the integration and can prove useful when debugging an integration.
|
||||||
|
|
||||||
|
We consider it a good practice to have the diagnostics implemented.
|
||||||
|
Something to keep in mind is that the diagnostics should not expose any sensitive information, such as passwords, tokens, or coordinates.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the following example we provide diagnostics which includes data from various sources, such as the configuration and the current state of the integration.
|
||||||
|
Since the configuration may contain sensitive information, we redact the sensitive information before returning the diagnostics.
|
||||||
|
|
||||||
|
`diagnostics.py`:
|
||||||
|
```python showLineNumbers
|
||||||
|
TO_REDACT = [
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
]
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: MyConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||||
|
"data": entry.runtime_data.data,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: "Integration uses discovery info to update network information"
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Most networks of end users are using dynamic IP addresses.
|
||||||
|
This means that devices and services can get a different IP address than the one they had when they were first set up.
|
||||||
|
To avoid the need for users to set devices to static IP addresses (which is not always possible), integrations should use the discovery information to update the network information of the device or service.
|
||||||
|
|
||||||
|
We should only update the IP address of a device or service if the integration is sure that the device or service is the same one as set up previously.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the following example we have an integration that uses mDNS to discover devices.
|
||||||
|
Every time a zeroconf discovery flow is started, the integration will set the unique ID of the flow to the serial number of the device.
|
||||||
|
If the unique ID is already set, the device IP address will be updated if it has changed, and the flow will be aborted.
|
||||||
|
|
||||||
|
`manifest.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"zeroconf": ["_mydevice._tcp.local."]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {14-15} showLineNumbers
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self.data: dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
self.data[CONF_HOST] = host = discovery_info.host
|
||||||
|
|
||||||
|
await self.async_set_unique_id(discovery_info.properties["serialno"])
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||||
|
|
||||||
|
client = MyClient(host)
|
||||||
|
try:
|
||||||
|
await client.get_data()
|
||||||
|
except MyClientError:
|
||||||
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If you are using DHCP discovery, and you want to receive discovery flows for updated IP addresses, be sure to register the MAC address in the device info and set `registered_devices` to `true` in the manifest.
|
||||||
|
This will create discovery flows for those devices.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
To learn more information about config flows, checkout the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
To learn more about network protocols and discovery, checkout the [Networking and discovery documentation](../../../network_discovery).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
The exception to this rule is that not every device can be discovered.
|
||||||
|
Integrations where the devices can't be discovered are exempt from this rule.
|
||||||
|
|
126
docs/core/integration-quality-scale/rules/discovery.md
Normal file
126
docs/core/integration-quality-scale/rules/discovery.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
title: "Can be discovered"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- test-before-configure
|
||||||
|
- unique-config-entry
|
||||||
|
- config-flow-test-coverage
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
A lot of devices have the ability to be discovered.
|
||||||
|
This can happen using one of the following methods:
|
||||||
|
- Add-on
|
||||||
|
- Bluetooth
|
||||||
|
- DHCP
|
||||||
|
- HomeKit
|
||||||
|
- mDNS
|
||||||
|
- MQTT
|
||||||
|
- SSDP
|
||||||
|
- USB
|
||||||
|
|
||||||
|
This is a great way to make it easier for users to find and set up devices, since they don't have to manually look up which integration to use and then enter the host.
|
||||||
|
This greatly reduces the effort required to set up a device and thus improves the user experience.
|
||||||
|
|
||||||
|
Using a network-based setup, also allows the configuration of the integration to be updated once the device receives a new IP address.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the following example, the integration is discoverable using mDNS.
|
||||||
|
The device would make itself discoverable by providing a `_mydevice._tcp.local.` service.
|
||||||
|
Home Assistant will pick this up and start a discovery flow for the user.
|
||||||
|
The user will then be able to confirm the discovery and set up the integration.
|
||||||
|
|
||||||
|
`manifest.json`:
|
||||||
|
```json {2} showLineNumbers
|
||||||
|
{
|
||||||
|
"zeroconf": ["_mydevice._tcp.local."]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {8-23,25-36} showLineNumbers
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self.data: dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
self.data[CONF_HOST] = host = discovery_info.host
|
||||||
|
|
||||||
|
await self.async_set_unique_id(discovery_info.properties["serialno"])
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||||
|
|
||||||
|
client = MyClient(host)
|
||||||
|
try:
|
||||||
|
await client.get_data()
|
||||||
|
except MyClientError:
|
||||||
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
|
async def async_step_discovery_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm discovery."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data={CONF_HOST: self.data[CONF_HOST]},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(step_id="discovery_confirm")
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST])
|
||||||
|
try:
|
||||||
|
serial_number = await client.check_connection()
|
||||||
|
except MyException as exception:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
serial_number, raise_on_progress=False
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): TextSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
To learn more information about config flows, checkout the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
To learn more about discovery on network protocols, checkout the [Networking and discovery documentation](../../../network_discovery).
|
||||||
|
To learn more about discovery for bluetooth devices, checkout the [Bluetooth documentation](../../../bluetooth).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
The exception to this rule is that not every device can be discovered.
|
||||||
|
Integrations where the devices can't be discovered are exempt from this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
27
docs/core/integration-quality-scale/rules/docs-actions.md
Normal file
27
docs/core/integration-quality-scale/rules/docs-actions.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes the provided service actions that can be used"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Integrations can register service actions to provide functionality that is not possible with standard entities.
|
||||||
|
These service actions can be harder to use than the standard service actions, so we want to make sure that the documentation describes both what they do, and what the parameters are.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
The integration provides the following actions.
|
||||||
|
|
||||||
|
### Action: Get schedule
|
||||||
|
|
||||||
|
The `my_integration.get_schedule` service is used to fetch a schedule from the integration.
|
||||||
|
|
||||||
|
| Data attribute | Optional | Description |
|
||||||
|
|------------------------|----------|------------------------------------------------------|
|
||||||
|
| `config_entry_id` | No | The ID of the config entry to get the schedule from. |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes all integration configuration options"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Integrations can provide an options flow to allow users to change integration configuration.
|
||||||
|
This rule ensures that all configuration options are documented so that users can understand what each option does and how to use it.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
The following example is for an integration with multiple configuration options, using the `configuration_basic` tag.
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Configuration options
|
||||||
|
|
||||||
|
The integration provides the following configuration options:
|
||||||
|
|
||||||
|
{% configuration_basic %}
|
||||||
|
Country code:
|
||||||
|
description: You can specify the country code (NL or BE) of the country to display on the camera.
|
||||||
|
Timeframe:
|
||||||
|
description: Minutes to look ahead for precipitation forecast sensors (minimum 5, maximum 120).
|
||||||
|
{% end configuration_basic %}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes how data is updated"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
For the user to know how the integration works, we should describe how the data of the integration is updated.
|
||||||
|
Because this will help users create an expectation on how well the integration works for their use case.
|
||||||
|
A motion sensor that only polls every 5 minutes is less usable than a motion sensor that actively pushes updates.
|
||||||
|
|
||||||
|
Since users can define their own polling interval for polling integrations, we should add at what rate we poll now and describe any limitations.
|
||||||
|
For example if the device we connect to has known problems handling too many requests, we should describe that in the documentation.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Data updates
|
||||||
|
|
||||||
|
My integration fetches data from the device every 5 minutes by default.
|
||||||
|
Newer devices (the ones running MyOS) have the possibility to {% term push %} data.
|
||||||
|
At the start of the integration we try to enable that, and if it fails we fall back to {% term polling %}.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
26
docs/core/integration-quality-scale/rules/docs-examples.md
Normal file
26
docs/core/integration-quality-scale/rules/docs-examples.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation provides automation examples the user can use."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
To show how the integration can be used, we should provide a limited set of blueprints, containing common or useful ones.
|
||||||
|
This will help users to get started with the integration faster and easier.
|
||||||
|
|
||||||
|
The documentation pages should not be used as a collection or as a replacement of the blueprint exchange on the forums.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Turning off the LEDs during the night
|
||||||
|
The status LEDs on the device can be quite bright.
|
||||||
|
To tackle this, you can use this blueprint to easily automate the LEDs turning off when the sun goes down.
|
||||||
|
|
||||||
|
link to blueprint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation includes a high-level description of the integration brand, product, or service"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
User documentation for an integration should provide a high-level description of the integration brand, product, or service.
|
||||||
|
This information might help users decide whether an integration suits them and their use case.
|
||||||
|
The documentation should also contain a link to the brand, product, or service website for further information, if possible.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
The **my integration** {% term integration %} is used to integrate with the devices of [MyCompany](https://www.mycompany.com).
|
||||||
|
They create various smart home appliances and devices and are known for their MyProduct.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
Integrations that do not integrate with a device or service, such as internal integrations, can't include a description of the device or service, and are exempt.
|
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation provides step-by-step installation instructions for the integration, including, if needed, prerequisites"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
We want users to have a smooth experience when setting up an integration.
|
||||||
|
This means that the documentation should provide clear and concise instructions on how to install the integration.
|
||||||
|
This includes any prerequisites that are needed to install the integration.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Open the app store and install the **MyProduct** app.
|
||||||
|
2. Create an account.
|
||||||
|
3. Add a device to the app.
|
||||||
|
4. Open the app and go to the **Settings** page.
|
||||||
|
5. Select **Expose API**.
|
||||||
|
|
||||||
|
{% include integrations/config_flow.md %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes all integration installation parameters"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
When setting up an integration, there's nothing more frustrating than not knowing what information is asked for.
|
||||||
|
To improve the user experience, the documentation should describe all the parameters that are required during the installation process.
|
||||||
|
This should help the user to gather all the necessary information before starting the installation process.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
{% configuration_basic %}
|
||||||
|
Host:
|
||||||
|
description: "The IP address of your bridge. You can find it in your router or in the Integration app under **Bridge Settings** -> **Local API**."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
Local access token:
|
||||||
|
description: "The local access token for your bridge. You can find it in the Integration app under **Bridge Settings** -> **Local API**."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
{% endconfiguration_basic %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes known limitations of the integration (not to be confused with bugs)"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Describing the known limitations of the integration in the documentation will allow users to know what to expect from the integration.
|
||||||
|
|
||||||
|
We should refrain from noting down bugs, since we use the issue tracker at GitHub for that and we don't want to duplicate information.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
The integration does not provide the ability to reboot, which can instead be done via the manufacturer's app.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation provides removal instructions"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Removing a device or service from Home Assistant isn't always straightforward.
|
||||||
|
The documentation should provide clear instructions on how to remove the device or service.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
To remove the integration, go to {% my integrations title="**Settings** > **Devices & services**" %} and select the integration card. Then, select the three dots {% icon "mdi:dots-vertical" %} menu and select **Delete**.
|
||||||
|
After deleting the integration, go to the app of the manufacturer and remove the Home Assistant integration from there as well.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: "The integration documents known supported / unsupported devices"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
A lot of Home Assistant users buy devices based on if Home Assistant supports them.
|
||||||
|
To make it easier for users to find out if a device is supported, the documentation should document the known supported or unsupported devices.
|
||||||
|
This will decrease the amount of bad experiences where the user finds out their device is not supported when they try to set it up.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Supported devices
|
||||||
|
|
||||||
|
The following devices are known to be supported by the integration:
|
||||||
|
- Device 1
|
||||||
|
- Device 2
|
||||||
|
- Every appliance that runs MyOS
|
||||||
|
|
||||||
|
## Unsupported devices
|
||||||
|
|
||||||
|
The following devices are not supported by the integration:
|
||||||
|
- Device 3
|
||||||
|
- Appliances built before 2010
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
This rule does not apply to integrations that do not connect to a device or service.
|
||||||
|
This rule also does not apply to integrations that don't integrate physical devices.
|
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes the supported functionality, including entities, and platforms"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Users should be able to understand what value the integration will add for their (to be bought) device.
|
||||||
|
This will help set users' expectations.
|
||||||
|
|
||||||
|
For example, if a user is looking for a new fridge, we should try to be clear about what they can expect from the integration.
|
||||||
|
If the integration only supports checking if the door is open or closed, they would be disappointed if they expected to be able to view the temperature of the fridge.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
Example, sorted by entity types:
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Supported functionality
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
|
||||||
|
The XY integration provides the following entities.
|
||||||
|
|
||||||
|
#### Buttons
|
||||||
|
|
||||||
|
- **Start backflush**
|
||||||
|
- **Description**: Starts the backflush process on your machine. You got 15 seconds to turn the paddle after activation.
|
||||||
|
- **Available for machines**: all
|
||||||
|
|
||||||
|
#### Numbers
|
||||||
|
|
||||||
|
- **Dose**
|
||||||
|
- **Description**: Dosage (in ticks) for each key
|
||||||
|
- **Available for machines**: GS3 AV, Linea Mini.
|
||||||
|
- **Remarks**: GS3 has this multiple times, one for each physical key (1-4), and the entities are disabled by default.
|
||||||
|
|
||||||
|
#### Sensors
|
||||||
|
|
||||||
|
- **Current coffee temperature**
|
||||||
|
- **Description**: Current temperature of the coffee boiler.
|
||||||
|
- **Available for machines**: all
|
||||||
|
- **Remarks**: When the machine reaches temperature, this will be approximately 3 degrees higher than the `Coffee target temperature`, due to different measurement points.
|
||||||
|
|
||||||
|
- **Current steam temperature**
|
||||||
|
- **Description**: Current temperature of the steam boiler.
|
||||||
|
- **Available for machines**: Linea Micra, GS3 AV, GS3 MP.
|
||||||
|
- **Remarks**: -
|
||||||
|
|
||||||
|
#### Updates
|
||||||
|
|
||||||
|
- **Gateway firmware**
|
||||||
|
- **Description**: Firmware status of the gateway.
|
||||||
|
- **Available for machines**: all
|
||||||
|
|
||||||
|
#### Selects
|
||||||
|
|
||||||
|
- **Prebrew/-infusion mode**
|
||||||
|
- **Description**: Whether to use prebrew, preinfusion, or neither.
|
||||||
|
- **Options**: Disabled, Prebrew, Preinfusion
|
||||||
|
- **Available for machines**: Linea Micra, Linea Mini, GS3 AV
|
||||||
|
|
||||||
|
- **Steam level**
|
||||||
|
- **Description**: The level your steam boiler should run at.
|
||||||
|
- **Options**: 1, 2, 3
|
||||||
|
- **Available for machines**: Linea Micra
|
||||||
|
```
|
||||||
|
|
||||||
|
Example, sorted by device:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Supported functionality
|
||||||
|
|
||||||
|
### XYZ productname Air Purifier, Air Humidifier and Standing Fan
|
||||||
|
|
||||||
|
#### Sensors
|
||||||
|
|
||||||
|
- **Filter lifetime remaining**: The remaining life of the filter in number of years. Enabled by default.
|
||||||
|
- **Purify volume**: The volume of purified air in cubic meters. Disabled by default.
|
||||||
|
|
||||||
|
#### Numbers
|
||||||
|
|
||||||
|
- **Favorite level**: Set the favorite level. Possible values are 0 to 10. `0` means it is turned off.)
|
||||||
|
- **Volume**: Set the volume. In percent. `0%` means it is off.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation provides troubleshooting information"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
We should provide instructions on how to fix a common issue.
|
||||||
|
If possible, a troubleshooting topic should include a description of the symptom and the steps needed to fix the situation.
|
||||||
|
This decreases the amount of support requests and improves the user experience.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Can’t setup the device
|
||||||
|
|
||||||
|
#### Symptom: “This device can’t be reached”
|
||||||
|
|
||||||
|
When trying to setup the integration, the form shows the message “This device can’t be reached”.
|
||||||
|
|
||||||
|
##### Description
|
||||||
|
|
||||||
|
This means the settings on the device are incorrect, since the device needs to be enabled for local communication.
|
||||||
|
|
||||||
|
##### Resolution
|
||||||
|
|
||||||
|
To resolve this issue, try the following steps:
|
||||||
|
|
||||||
|
1. Make sure your device is powered up (LEDs are on).
|
||||||
|
2. Make sure your device is connected to the internet:
|
||||||
|
- Make sure the app of the manufacturer can see the device.
|
||||||
|
3. Make sure the device has the local communication enabled:
|
||||||
|
- Check the device’s settings.
|
||||||
|
- Check the device’s manual.
|
||||||
|
...
|
||||||
|
|
||||||
|
### I can't see my devices
|
||||||
|
Make sure the devices are visible and controllable via the manufacturer's app.
|
||||||
|
If they are not, check the device's power and network connection.
|
||||||
|
|
||||||
|
### The device goes unavailable after a day
|
||||||
|
Make sure you turned off the device's power-saving mode.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
21
docs/core/integration-quality-scale/rules/docs-use-cases.md
Normal file
21
docs/core/integration-quality-scale/rules/docs-use-cases.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: "The documentation describes use cases to illustrate how this integration can be used"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Sometimes, seeing a device or service integrated into Home Assistant can make you wonder, "Why would I integrate this?"
|
||||||
|
For some integrations, the intended use and benefit are more obvious than for others.
|
||||||
|
|
||||||
|
Use case examples in the documentation can showcase the value of an integration.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
```markdown showLineNumbers
|
||||||
|
The motion detection devices of MyCompany are cheap and usable.
|
||||||
|
When you combine it with their other device you can do x.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
73
docs/core/integration-quality-scale/rules/dynamic-devices.md
Normal file
73
docs/core/integration-quality-scale/rules/dynamic-devices.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
title: "Devices added after integration setup"
|
||||||
|
related_rules:
|
||||||
|
- stale-devices
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Like explained in IQS021, devices should be removed automatically when we can be sure that the device is not connected anymore.
|
||||||
|
This rule explains the other side, once a new device is connected, we should automatically create the relevant entities for the device.
|
||||||
|
|
||||||
|
This makes the user experience better, since the user only adds the device to the integration, and it will automatically show up in Home Assistant.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below we use a coordinator to fetch all the data from the service.
|
||||||
|
Every update `_check_device` will check if there are new devices to create entities for and add them to Home Assistant.
|
||||||
|
|
||||||
|
`coordinator.py`
|
||||||
|
```python showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[dict[str, MyDevice]]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: MyClient) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, MyDevice]:
|
||||||
|
try:
|
||||||
|
return await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
raise UpdateFailed(f"The service is unavailable: {ex}")
|
||||||
|
```
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {9,11-16,18-21} showLineNumbers
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: MyConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up My integration from a config entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
|
def _check_device() -> None:
|
||||||
|
current_devices = set(coordinator.data)
|
||||||
|
new_devices = current_devices - known_devices
|
||||||
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities([MySensor(coordinator, device_id) for device_id in new_devices])
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
entry.async_on_unload(
|
||||||
|
coordinator.async_add_listener(_check_device)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
30
docs/core/integration-quality-scale/rules/entity-category.md
Normal file
30
docs/core/integration-quality-scale/rules/entity-category.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: "Entities are assigned an appropriate EntityCategory"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Entities should be assigned an appropriate EntityCategory to ensure that they are correctly classified and can be easily identified, when the default category is inappropriate.
|
||||||
|
The entity category is used in, for example, auto-generated dashboards.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, we have a sensor that returns a diagnostic value.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {4} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
def __init__(self, ...) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
To learn more about the registry properties, checkout the [documentation](../../entity#registry-properties) about it.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: "Entities use device classes where possible"
|
||||||
|
related_rules:
|
||||||
|
- has-entity-name
|
||||||
|
- entity-translations
|
||||||
|
- icon-translations
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Device classes are a way to give context to an entity.
|
||||||
|
These are used by Home Assistant for various purposes like:
|
||||||
|
- Allowing the user to switch to another unit of measurement than what the device provides.
|
||||||
|
- They are used for voice control to ask questions like "What is the temperature in the living room?".
|
||||||
|
- They are used for exposing entities to cloud based ecosystems like Google Assistant and Amazon Alexa.
|
||||||
|
- They are used to adjust the representation in the Home Assistant UI.
|
||||||
|
- They can be used to set a default name of the entity to decrease the burden on our translators.
|
||||||
|
|
||||||
|
Because of these reasons, it is important to use device classes where possible.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below we have a temperature sensor that uses the device class `temperature`.
|
||||||
|
The name of this entity will be `My device temperature`.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {5} showLineNumbers
|
||||||
|
class MyTemperatureSensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||||
|
|
||||||
|
def __init__(self, device: Device) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device.id)},
|
||||||
|
name="My device",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
A list of available device classes can be found in the entity pages under the [entity](../../entity) page.
|
||||||
|
More information about entity naming can be found in the [entity](../../entity#has_entity_name-true-mandatory-for-new-integrations) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: "Integration disables less popular (or noisy) entities"
|
||||||
|
related_rules:
|
||||||
|
- appropriate-polling
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Home Assistant keeps track of how the states of entities changes.
|
||||||
|
This is done to be able to show the history of the entity in the UI.
|
||||||
|
Every state that is tracked takes up a bit of resources.
|
||||||
|
Entities that change state a lot (noisy entities), do this more often than entities that change state less often.
|
||||||
|
|
||||||
|
We consider it a good practice to disable less popular or noisy entities by default.
|
||||||
|
If users have a use case for such an entity, they can enable it.
|
||||||
|
This way users that don't have a use case for the entity, don't have to pay the cost of tracking the state of the entity.
|
||||||
|
|
||||||
|
There is no hard rule on what is considered a popular entity, since that depends on the integration and the device.
|
||||||
|
So for example, a bluetooth temperature sensor can have an entity that represents the signal strength of the device.
|
||||||
|
This entity is not very useful for most users, so it should be disabled by default.
|
||||||
|
While if there was an integration providing a device to measure signal strength, that entity would be useful for most users and should be enabled by default.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, the entity is disabled by default.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {8} showLineNumbers
|
||||||
|
class MySignalStrengthSensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH
|
||||||
|
_attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
|
_attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
|
def __init__(self, device: Device) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
To learn more about the entity registry properties, checkout the [documentation](../../entity#registry-properties) about it.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: "Entities event setup"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Entities may need to subscribe to events, eg. from the integration library, and update state when a new event comes in.
|
||||||
|
In order to do this correctly, the entities should subscribe and register the update callback in the entity method `async_added_to_hass`.
|
||||||
|
This entity method is called after the entity has been registered by the entity platform helper and the entity will now have all its interfaces available to call, such as `self.hass` and `self.async_write_ha_state`.
|
||||||
|
Registering an update callback before this stage will cause errors if the callback eg. tries to access `self.hass` or write a state update.
|
||||||
|
To avoid memory leaks, the entities should unsubscribe from the events, ie. unregister the update callback, in the entity method `async_will_remove_from_hass`.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, the `self.client.events.subscribe` returns a function that when called, unsubscribes the entity from the event.
|
||||||
|
So we subscribe to the event in `async_added_to_hass` and unsubscribe in `async_will_remove_from_hass`.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {10-13,15-19} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
unsubscribe: Callable[[], None] = None
|
||||||
|
|
||||||
|
def __init__(self, client: MyClient) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to the events."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self.unsubscribe = self.client.events.subscribe("my_event", self._handle_event)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Unsubscribe from the events."""
|
||||||
|
if self.unsubscribe:
|
||||||
|
self.unsubscribe()
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
|
async def _handle_event(self, event: Event) -> None:
|
||||||
|
"""Handle the event."""
|
||||||
|
...
|
||||||
|
self.async_write_ha_state()
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
The above example can be simplified using lifecycle functions.
|
||||||
|
This saves the need to store the callback function in the entity.
|
||||||
|
```python showLineNumbers
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to the events."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self.async_on_remove(
|
||||||
|
self.client.events.subscribe("my_event", self._handle_event)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: "Entities have translated names"
|
||||||
|
related_rules:
|
||||||
|
- has-entity-name
|
||||||
|
- entity-device-class
|
||||||
|
- icon-translations
|
||||||
|
- exception-translations
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Home Assistant is used by people all over the world.
|
||||||
|
To also make it easier for non-English speakers to use Home Assistant, it is important that entities have translated names.
|
||||||
|
This makes it easier for people to understand what the entity is.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, the sensor has the name "Phase voltage" in English.
|
||||||
|
Combined with the device name, this entity will name itself "My device Phase voltage".
|
||||||
|
|
||||||
|
`sensor.py`:
|
||||||
|
```python {5} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_translation_key = "phase_voltage"
|
||||||
|
|
||||||
|
def __init__(self, device_id: str) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device_id)},
|
||||||
|
name="My device",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`strings.json`:
|
||||||
|
```json {5} showLineNumbers
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"phase_voltage": {
|
||||||
|
"name": "Phase voltage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If the entity's platform is either `binary_sensor`, `number`, `sensor`, or `update` and it has a device class set, and you want the entity to have the same name as the device class, you can omit the translation key because the entity will then automatically use the device class name.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about the translation process can be found in the [translation](../../../internationalization/core) documentation, it also contains information about the [entity translations](../../../internationalization/core#name-of-entities).
|
||||||
|
More information about entity naming can be found in the [entity](../../entity#has_entity_name-true-mandatory-for-new-integrations) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: "Mark entity unavailable if appropriate"
|
||||||
|
related_rules:
|
||||||
|
- log-when-unavailable
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
If we can't fetch data from a device or service, we should mark it as unavailable.
|
||||||
|
We do this to reflect a better state, than just showing the last known state.
|
||||||
|
|
||||||
|
If we can successfully fetch data but are temporarily missing a few pieces of data, we should mark the entity state as unknown instead.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
Since there are many different ways this can be implemented, we will only provide the example for integrations using the coordinator and for entities updating via `async_update`.
|
||||||
|
|
||||||
|
### Example for integrations using the coordinator
|
||||||
|
|
||||||
|
In this example, we have an integration that uses a coordinator to fetch data.
|
||||||
|
The coordinator, when combined with a `CoordinatorEntity` has the logic for availability built-in.
|
||||||
|
If there is any extra availability logic needed, be sure to incorporate the `super().available` value.
|
||||||
|
In the sensor in the example, we mark the entity unavailable when the update fails, or when the data for that device is missing.
|
||||||
|
|
||||||
|
`coordinator.py`
|
||||||
|
```python {18} showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[dict[str, MyDevice]]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: MyClient) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, MyDevice]:
|
||||||
|
try:
|
||||||
|
return await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
raise UpdateFailed(f"The service is unavailable: {ex}")
|
||||||
|
```
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {6} showLineNumbers
|
||||||
|
class MySensor(SensorEntity, CoordinatorEntity[MyCoordinator]):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return super().available and self.identifier in self.coordinator.data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example for entities updating via `async_update`
|
||||||
|
|
||||||
|
In this example, we have a sensor that updates its value via `async_update`.
|
||||||
|
If we can't fetch the data, we set the entity as unavailable using shorthand notation.
|
||||||
|
If we can fetch the data, we set the entity as available and update the value.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {7,9} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
try:
|
||||||
|
data = await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
self._attr_available = False
|
||||||
|
else:
|
||||||
|
self._attr_available = True
|
||||||
|
self._attr_native_value = data.value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about managing integration state, see the [documentation](../../../integration_fetching_data).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: "Entities have a unique ID"
|
||||||
|
related_rules:
|
||||||
|
- unique-config-entry
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
In the past, entities weren't persisted.
|
||||||
|
Home Assistant didn't track which entities it knew from the past and which it did not.
|
||||||
|
To allow customizations to entities, like renaming the entity or changing the unit of measurement, Home Assistant needed a way to keep track of each individual entity across restarts.
|
||||||
|
|
||||||
|
To solve this, Home Assistant introduced the entity registry.
|
||||||
|
The entity registry is a central place where Home Assistant keeps track of all entities it knows about.
|
||||||
|
Each entity in the entity registry has a unique ID, which is unique per integration domain and per platform domain.
|
||||||
|
|
||||||
|
If an entity doesn't have a unique ID, the user has less control over the entity.
|
||||||
|
Thus, making sure that entities have a unique ID improves the user experience.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example there is a temperature sensor that sets its unique ID using the shorthand notation.
|
||||||
|
|
||||||
|
`sensor.py`:
|
||||||
|
```python {6} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
def __init__(self, device_id: str) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._attr_unique_id = f"{device_id}_temperature"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about the requirements for a unique identifier can be found in the [documentation](../../../entity_registry_index#unique-id-requirements).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: "Exception messages are translatable"
|
||||||
|
related_rules:
|
||||||
|
- entity-translations
|
||||||
|
- action-exceptions
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Sometimes something goes wrong and we want to show an error message to the user.
|
||||||
|
Since Home Assistant is used by people all over the world, it is important that these error messages are translatable.
|
||||||
|
This increases the usability of Home Assistant for people who do not use the application in English.
|
||||||
|
|
||||||
|
Home Assistant has builtin support for translating messages coming from the `HomeAssistantError` exception.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, we show a function registered as a Home Assistant service action.
|
||||||
|
The integration domain and the key to the translation are passed along when raising the exception.
|
||||||
|
The exception should inherit `HomeAssistantError` to support translations.
|
||||||
|
The error message is then defined in the integration `strings.json` file.
|
||||||
|
|
||||||
|
```python {6-9,13-16} showLineNumbers
|
||||||
|
async def async_set_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
|
"""Set the schedule for a day."""
|
||||||
|
start_date = call.data[ATTR_START_DATE]
|
||||||
|
end_date = call.data[ATTR_END_DATE]
|
||||||
|
if end_date < start_date:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="end_date_before_start_date",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await client.set_schedule(start_date, end_date)
|
||||||
|
except MyConnectionError as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect_to_schedule",
|
||||||
|
) from err
|
||||||
|
```
|
||||||
|
|
||||||
|
`strings.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exceptions": {
|
||||||
|
"end_date_before_start_date": "The end date cannot be before the start date.",
|
||||||
|
"cannot_connect_to_schedule": "Cannot connect to the schedule."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more info on raising exceptions, check the [documentation](../../platform/raising_exceptions).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
68
docs/core/integration-quality-scale/rules/has-entity-name.md
Normal file
68
docs/core/integration-quality-scale/rules/has-entity-name.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: "Entities use has_entity_name = True"
|
||||||
|
related_rules:
|
||||||
|
- entity-translations
|
||||||
|
- entity-device-class
|
||||||
|
- devices
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
`has_entity_name` is an entity attribute that is used to improve the naming of entities in Home Assistant.
|
||||||
|
It is introduced to show a better name of the entity to the user depending on the context where the name is shown.
|
||||||
|
|
||||||
|
We consider this a good practice because it allows for consistency in naming between integrations.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, if the name of the device is "My device" and the field is "temperature", the name of the entity will be shown as "My device temperature".
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {4} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, device: Device, field: str) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device.id)},
|
||||||
|
name=device.name,
|
||||||
|
)
|
||||||
|
self._attr_name = field
|
||||||
|
```
|
||||||
|
|
||||||
|
However, when the name of the entity is set to `None`, the name of the device will be used as the name of the entity.
|
||||||
|
In this case, the lock entity will just be called "My device".
|
||||||
|
This should be done for the main feature of the device.
|
||||||
|
|
||||||
|
`lock.py`
|
||||||
|
```python {4-5,11} showLineNumbers
|
||||||
|
class MyLock(LockEntity):
|
||||||
|
"""Representation of a lock."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
def __init__(self, device: Device) -> None:
|
||||||
|
"""Initialize the lock."""
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device.id)},
|
||||||
|
name=device.name,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about entity naming can be found in the [entity](../../entity#has_entity_name-true-mandatory-for-new-integrations) documentation.
|
||||||
|
More information about devices can be found in the [device](../../../device_registry_index) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: "Icon translations"
|
||||||
|
related_rules:
|
||||||
|
- entity-translations
|
||||||
|
- entity-device-class
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
In the past, icons were part of the state of the integration.
|
||||||
|
This was not really necessary, as they were usually either static or had a fixed set of states.
|
||||||
|
|
||||||
|
To relieve the state machine, icon translations were introduced.
|
||||||
|
The name of this feature sounds weird since it is not about translating the icon itself, but rather referencing an icon by a translation key.
|
||||||
|
The idea behind icon translations is that the integration defines icons in a file, which is then used by the frontend to display the icon.
|
||||||
|
This also adds support for different icons for state attribute values, for example the possible preset modes of a climate entity.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Be aware that entities can also get icons from the device class.
|
||||||
|
If the context of the entity is exactly the same as the device class, we should not overwrite this icon to maintain consistency between integrations.
|
||||||
|
For example, a PM2.5 sensor entity would not get a custom icon, as the device class already provides it in the same context.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, we define a sensor entity with a translation key.
|
||||||
|
In the `icons.json` file, we define the icon for the sensor entity and a state icon for the state `high`.
|
||||||
|
So when the state of the entity is `high`, we will show the icon `mdi:tree-outline`, otherwise we will show `mdi:tree`.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {5} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_translation_key = "tree_pollen"
|
||||||
|
```
|
||||||
|
|
||||||
|
`icons.json`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"tree_pollen": "mdi:tree",
|
||||||
|
"state": {
|
||||||
|
"high": "mdi:tree-outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about icon translations, check the [entity](../../entity#icon-translations) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: "The integration dependency supports passing in a websession"
|
||||||
|
related_rules:
|
||||||
|
- async-dependency
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Since many devices and services are connected via HTTP, the number of active web sessions can be high.
|
||||||
|
To improve the efficiency of those web sessions, it is recommended to support passing in a web session to the dependency client that is used by the integration.
|
||||||
|
|
||||||
|
Home Assistants supports this for [`aiohttp`](https://docs.aiohttp.org/en/stable/) and [`httpx`](https://www.python-httpx.org/).
|
||||||
|
This means that the integration dependency should use either of those two libraries.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, an `aiohttp` session is passed in to the client.
|
||||||
|
The equivalent for `httpx` would be `get_async_client`.
|
||||||
|
|
||||||
|
```python {4} showLineNumbers
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
|
||||||
|
"""Set up my integration from a config entry."""
|
||||||
|
|
||||||
|
client = MyClient(entry.data[CONF_HOST], async_get_clientsession(hass))
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
There are cases where you might not want a shared session, for example when cookies are used.
|
||||||
|
In that case, you can create a new session using `async_create_clientsession` for `aiohttp` and `create_async_httpx_client` for `httpx`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
If the integration is not making any HTTP requests, this rule does not apply.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: "Has an integration owner"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Home Assistant integrates with thousands of different devices and services, and most integrations are contributed by people other than the core maintainers of the project.
|
||||||
|
The contributors that add and maintain an integration are encouraged to become the "integration owner".
|
||||||
|
This is a role that grants the contributor more power when handling issues and pull requests for the integration in GitHub and it means that the contributor has taken on the responsibility for the stewardship of the integration.
|
||||||
|
Integration owners will automatically get notified whenever there is a new issue or pull request for their integration.
|
||||||
|
On GitHub the integration owner is referred to as the "codeowner".
|
||||||
|
|
||||||
|
Integration owners are tracked in the `manifest.json` file of each integration.
|
||||||
|
To become an integration owner, submit a pull request adding your GitHub username to the `"codeowners"` field in the manifest.
|
||||||
|
An integration can have more than one owner.
|
||||||
|
|
||||||
|
We love integration owners!
|
||||||
|
We believe that integrations that have an owner are better maintained.
|
||||||
|
During reviews, we see the integration owner as the expert on the integration, and weigh their opinion higher than others.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
Integration owners are set in the `manifest.json`.
|
||||||
|
|
||||||
|
```json {3} showLineNumbers
|
||||||
|
{
|
||||||
|
"domain": "my_integration",
|
||||||
|
"name": "My Integration",
|
||||||
|
"codeowners": ["@me"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about integration owners can be found in [ADR-0008](https://github.com/home-assistant/architecture/blob/master/adr/0008-code-owners.md).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
title: "If internet/device/service is unavailable, log once when unavailable and once when back connected"
|
||||||
|
related_rules:
|
||||||
|
- entity-unavailable
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
When a device or service is not reachable, the entities will usually go to unavailable.
|
||||||
|
To allow the user to find out why this is happening, the integration should log when this happens.
|
||||||
|
Be sure to log only once in total to avoid spamming the logs.
|
||||||
|
|
||||||
|
When the device or service is reachable again, the integration should log that as well.
|
||||||
|
This can prove useful for using the logs to find out when the device or service was unavailable and when it was back online.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Logging should happen at `info` level.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
Since there are many different ways this can be implemented, we will only provide the example for integrations using the coordinator and for entities updating via `async_update`.
|
||||||
|
|
||||||
|
### Example for integrations using the coordinator
|
||||||
|
|
||||||
|
In this example, we have an integration that uses a coordinator to fetch data.
|
||||||
|
The coordinator has the logic for logging once built in.
|
||||||
|
The only thing that you need to do in the coordinator is to raise `UpdateFailed` when the device or service is unavailable.
|
||||||
|
|
||||||
|
`coordinator.py`
|
||||||
|
```python {18} showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: MyClient) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> MyData:
|
||||||
|
try:
|
||||||
|
return await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
raise UpdateFailed(f"The device is unavailable: {ex}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example for entities updating via `async_update`
|
||||||
|
|
||||||
|
In this example, we have a sensor that updates its value via `async_update`.
|
||||||
|
The example will log when the sensor is unavailable and log when the sensor is back online.
|
||||||
|
Note that an instance attribute is used to track if the message has been logged to avoid spamming the logs.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {10-12,16-18} showLineNumbers
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
|
||||||
|
_unavailable_logged: bool = False
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
try:
|
||||||
|
data = await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
self._attr_available = False
|
||||||
|
if not self._unavailable_logged:
|
||||||
|
_LOGGER.info("The sensor is unavailable: %s", ex)
|
||||||
|
self._unavailable_logged = True
|
||||||
|
else:
|
||||||
|
self._attr_available = True
|
||||||
|
self._attr_native_value = data.value
|
||||||
|
if self._unavailable_logged:
|
||||||
|
_LOGGER.info("The sensor is back online")
|
||||||
|
self._unavailable_logged = False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about managing integration state, see the [documentation](../../../integration_fetching_data)
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: "Set Parallel updates"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Some devices or services don't like receiving a lot of requests at the same time.
|
||||||
|
To avoid that, Home Assistant has a built-in feature to limit the number of requests that are sent to a device or service at the same time.
|
||||||
|
|
||||||
|
This will be applied to both entity updates and actions calls.
|
||||||
|
|
||||||
|
We consider it a good practice to explicitly set the number of parallel updates.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, we set the number of parallel updates to 1.
|
||||||
|
Which means if there are more entities on the sensor platform, they will be updated one by one.
|
||||||
|
If there is no need to limit the number of parallel updates, you can set it to 0.
|
||||||
|
|
||||||
|
`sensor.py`
|
||||||
|
```python {1} showLineNumbers
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
class MySensor(SensorEntity):
|
||||||
|
"""Representation of a sensor."""
|
||||||
|
|
||||||
|
def __init__(self, device: Device) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
:::info
|
||||||
|
When using a coordinator, you are already centralizing the data updates.
|
||||||
|
This means that usually only the action calls will be relevant to consider for setting the number of parallel updates.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about request parallelism, check the [documentation](../../../integration_fetching_data#request-parallelism) for it.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
title: "Reauthentication flow"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- test-before-configure
|
||||||
|
- config-flow-test-coverage
|
||||||
|
- test-before-setup
|
||||||
|
- reconfiguration-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
It can happen that users change their password of a device or service and forget that their device or account is still linked to Home Assistant.
|
||||||
|
To avoid that the user has to remove the configuration entry and re-add it, we start a reauthentication flow.
|
||||||
|
During this flow, the user can provide the new credentials to use from now on.
|
||||||
|
|
||||||
|
This is a very user-friendly way to let the user know that they need to take action and update their credentials.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below, we show an authentication flow that allows the user to reauthenticate with a new API token.
|
||||||
|
When we receive the new token, we check if we can connect to the service to avoid the user from entering an invalid token.
|
||||||
|
If the connection is successful, we update the configuration entry with the new token.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {6-11,13-35} showLineNumbers
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauthentication upon an API authentication error."""
|
||||||
|
self.host = entry_data[CONF_HOST]
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm reauthentication dialog."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(self.host, user_input[CONF_API_TOKEN])
|
||||||
|
try:
|
||||||
|
user_id = await client.check_connection()
|
||||||
|
except MyException as exception:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_id)
|
||||||
|
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(),
|
||||||
|
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): TextSelector()}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST], user_input[CONF_API_TOKEN])
|
||||||
|
try:
|
||||||
|
user_id = await client.check_connection()
|
||||||
|
except MyException as exception:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): TextSelector(),
|
||||||
|
vol.Required(CONF_API_TOKEN): TextSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more info about handling expired credentials, check the [documentation](../../../integration_setup_failures#handling-expired-credentials).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
If the integration doesn't require any form of authentication, this rule doesn't apply.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: "Integrations should have a reconfigure flow"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- test-before-configure
|
||||||
|
- unique-config-entry
|
||||||
|
- config-flow-test-coverage
|
||||||
|
- reauthentication-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
It can happen that a user changes something to a device or service, like changing passwords or changing the IP address.
|
||||||
|
Ideally, Home Assistant catches those events and lets the user know that it requires a reauthentication or attention.
|
||||||
|
A reconfigure flow gives users the power to trigger a reconfiguration and allows them to update the configuration of the device or service, without the need to remove and re-add the device or service.
|
||||||
|
|
||||||
|
This gives users more ways to try and fix their issues, without the need for the software to be restarted or reauthentication to be triggered.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the `config_flow.py` file, add a new step called `reconfigure` that allows users to reconfigure the integration.
|
||||||
|
In the following example, we check if the new api token is valid.
|
||||||
|
We also double-check if the user is not trying to reconfigure the integration with a different account, since the account used for the integration should not change.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {4-31} showLineNumbers
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration of the integration."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST], user_input[CONF_API_TOKEN])
|
||||||
|
try:
|
||||||
|
user_id = await client.check_connection()
|
||||||
|
except MyException as exception:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_id)
|
||||||
|
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reconfigure_entry(),
|
||||||
|
data_updates=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): TextSelector(),
|
||||||
|
vol.Required(CONF_API_TOKEN): TextSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST], user_input[CONF_API_TOKEN])
|
||||||
|
try:
|
||||||
|
user_id = await client.check_connection()
|
||||||
|
except MyException as exception:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): TextSelector(),
|
||||||
|
vol.Required(CONF_API_TOKEN): TextSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information on the reconfiguration flow, see the [reconfigure flow documentation](../../../config_entries_config_flow_handler#reconfigure).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
Integrations that don't have settings in their configuration flow are exempt from this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
48
docs/core/integration-quality-scale/rules/repair-issues.md
Normal file
48
docs/core/integration-quality-scale/rules/repair-issues.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "Repair issues and repair flows are used when user intervention is needed"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Repair issues and repair flows are a very user-friendly manner to let the user know something is wrong and that they can do something about it.
|
||||||
|
Repair issues are just a way to let the user know that they can fix it themselves, while repair flows can fix it for them.
|
||||||
|
|
||||||
|
Repair issues and repair flows should be actionable and informative about the problem.
|
||||||
|
Thus, we should not raise repair issues for just letting users know that something is wrong, which they can't fix themselves.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In the example below we have an integration for a locally hosted service.
|
||||||
|
On boot, we check if we support the version of the service that is running.
|
||||||
|
If we do not, we raise a repair issue where we let the user know that they should update their service before they can use the integration again.
|
||||||
|
|
||||||
|
`__init__.py`
|
||||||
|
```python {6-14} showLineNumbers
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> None:
|
||||||
|
"""Set up the integration from a config entry."""
|
||||||
|
client = MyClient(entry.data[CONF_HOST])
|
||||||
|
version = await client.get_version()
|
||||||
|
if version < MINIMUM_VERSION:
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"outdated_version",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.ERROR,
|
||||||
|
translation_key="outdated_version",
|
||||||
|
)
|
||||||
|
raise ConfigEntryError(
|
||||||
|
"Version of MyService is %s, which is lower than minimum version %s",
|
||||||
|
version,
|
||||||
|
MINIMUM_VERSION,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about repair issues and repair flows, see the [repairs](../../platform/repairs) documentation.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
48
docs/core/integration-quality-scale/rules/runtime-data.md
Normal file
48
docs/core/integration-quality-scale/rules/runtime-data.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "Use ConfigEntry.runtime_data to store runtime data"
|
||||||
|
related_rules:
|
||||||
|
- test-before-setup
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
The `ConfigEntry` object has a `runtime_data` attribute that can be used to store runtime data.
|
||||||
|
This is useful for storing data that is not persisted to the configuration file storage, but is needed during the lifetime of the configuration entry.
|
||||||
|
|
||||||
|
By using `runtime_data`, we maintain consistency for developers to store runtime data in a consistent and typed way.
|
||||||
|
Because of the added typing, we can use tooling to avoid typing mistakes.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
The type of a `ConfigEntry` can be extended with the type of the data put in `runtime_data`.
|
||||||
|
In the following example, we extend the `ConfigEntry` type with `MyClient`, which means that the `runtime_data` attribute will be of type `MyClient`.
|
||||||
|
|
||||||
|
`__init__.py`:
|
||||||
|
```python {1,4,9} showLineNumbers
|
||||||
|
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: MyIntegrationConfigEntry) -> bool:
|
||||||
|
"""Set up my integration from a config entry."""
|
||||||
|
|
||||||
|
client = MyClient(entry.data[CONF_HOST])
|
||||||
|
|
||||||
|
entry.runtime_data = client
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about configuration entries and their lifecycle can be found in the [config entry documentation](../../../config_entries_index).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
90
docs/core/integration-quality-scale/rules/stale-devices.md
Normal file
90
docs/core/integration-quality-scale/rules/stale-devices.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: "Clean up stale devices"
|
||||||
|
related_rules:
|
||||||
|
- dynamic-devices
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
When a device is removed from a hub or account, it should also be removed from Home Assistant.
|
||||||
|
This way, the user interface will not show devices that are no longer available.
|
||||||
|
|
||||||
|
We should only remove devices that we are sure are no longer available.
|
||||||
|
If you can't be sure if a device is still available, be sure to implement `async_remove_config_entry_device`.
|
||||||
|
This allows the user to delete the device from the device registry manually.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
In this example, we have a coordinator that fetches data from a service.
|
||||||
|
When the data is updated, we check if any devices have been removed.
|
||||||
|
If so, we remove them from the device registry.
|
||||||
|
This also causes all entities associated with the device to be removed.
|
||||||
|
|
||||||
|
`coordinator.py`
|
||||||
|
```python {13,20-30} showLineNumbers
|
||||||
|
class MyCoordinator(DataUpdateCoordinator[dict[str, MyDevice]]):
|
||||||
|
"""Class to manage fetching data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: MyClient) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
self.previous_devices: set[str] = set()
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, MyDevice]:
|
||||||
|
try:
|
||||||
|
data = await self.client.get_data()
|
||||||
|
except MyException as ex:
|
||||||
|
raise UpdateFailed(f"The service is unavailable: {ex}")
|
||||||
|
current_devices = set(data)
|
||||||
|
if (stale_devices := self.previous_devices - current_devices):
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
for device_id in stale_devices:
|
||||||
|
device = device_registry.async_get(identifiers={(DOMAIN, device_id)})
|
||||||
|
if device:
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_id=device.id,
|
||||||
|
remove_config_entry_id=self.config_entry.entry_id,
|
||||||
|
)
|
||||||
|
self.previous_devices = current_devices
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
To show a second example where someone can delete the device from the device registry manually, we implement `async_remove_config_entry_device` in `__init__.py`.
|
||||||
|
Having this function defined will enable the delete button on the device page in the UI.
|
||||||
|
In this example, the integration is only able to get updates for a device and not get a full list of connected devices, hence it can't automatically delete devices.
|
||||||
|
In `async_remove_config_entry_device`, we should implement a function that checks if the device is still available.
|
||||||
|
If it is not, we return `True` to allow the user to delete the device manually.
|
||||||
|
Here, we assume that the device is not working if we haven't got any updates for it in a while.
|
||||||
|
|
||||||
|
`__init__.py`
|
||||||
|
```python showLineNumbers
|
||||||
|
async def async_remove_config_entry_device(
|
||||||
|
hass: HomeAssistant, config_entry: MyConfigEntry, device_entry: dr.DeviceEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Remove a config entry from a device."""
|
||||||
|
return not any(
|
||||||
|
identifier
|
||||||
|
for identifier in device_entry.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
and identifier[1] in config_entry.runtime_data.data
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more info on devices, checkout the [device registry documentation](../../../device_registry_index).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
24
docs/core/integration-quality-scale/rules/strict-typing.md
Normal file
24
docs/core/integration-quality-scale/rules/strict-typing.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Strict typing"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Python is a dynamically typed language, which can be the source of many bugs.
|
||||||
|
By using type hints, you can catch bugs early and avoid introducing them.
|
||||||
|
|
||||||
|
Type hints are checked by mypy, a static type checker for Python.
|
||||||
|
Because of the way typing in Python works, and type hints being optional in Python, mypy will only check the code that it knows to be type annotated.
|
||||||
|
To improve on this, we recommend fully typing your library and making your library PEP-561 compliant.
|
||||||
|
This means that you need to add a `py.typed` file to your library.
|
||||||
|
This file tells mypy that your library is fully typed, after which it can read the type hints from your library.
|
||||||
|
|
||||||
|
In the Home Assistant codebase, you can add your integration to the [`.strict-typing`](https://github.com/home-assistant/core/blob/dev/.strict-typing) file, which will enable strict type checks for your integration.
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
To read more about the `py.typed` file, see [PEP-561](https://peps.python.org/pep-0561/).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: "Test a connection in the config flow"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- unique-config-entry
|
||||||
|
- config-flow-test-coverage
|
||||||
|
- discovery
|
||||||
|
- reauthentication-flow
|
||||||
|
- reconfiguration-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Apart from being very easy to use, config flows are also a great way to let the user know that something is not going to work when the configuration has been completed.
|
||||||
|
This can catch issues like:
|
||||||
|
- DNS issues
|
||||||
|
- Firewall issues
|
||||||
|
- Wrong credentials
|
||||||
|
- Wrong IP address or port
|
||||||
|
- Unsupported devices
|
||||||
|
|
||||||
|
Issues like this are often hard to debug once the integration is set up, so it's better to catch them early so users are not stuck with an integration that doesn't work.
|
||||||
|
|
||||||
|
Since this improves the user experience, it's required to test the connection in the config flow.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
To validate the user input, you can call your library with the data as you normally would and do a test call.
|
||||||
|
If the call fails, you can return an error message to the user.
|
||||||
|
|
||||||
|
In the following example, if the `client.get_data()` call raises a `MyException`, the user will see an error message that the integration is unable to connect.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {10-17} showLineNumbers
|
||||||
|
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""My config flow."""
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST])
|
||||||
|
try:
|
||||||
|
await client.get_data()
|
||||||
|
except MyException:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): TextSelector()}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config flows can be found in the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
Integrations that don't have a connection to a device or service (for example helpers) don't need to test a connection in the config flow and are exempt from this rule.
|
||||||
|
Integrations that rely on auto-discovery on runtime (like Google Cast) are also exempt from this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: "Check during integration initialization if we are able to set it up correctly"
|
||||||
|
related_rules:
|
||||||
|
- runtime-data
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
When we initialize an integration, we should check if we are able to set it up correctly.
|
||||||
|
This way we can immediately let the user know that it doesn't work.
|
||||||
|
|
||||||
|
Implementing these checks increases the confidence that the integration will work correctly and provides a user-friendly way to show errors.
|
||||||
|
This will improve the user experience.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
When the reason for the failure is temporary (like a temporary offline device), we should raise `ConfigEntryNotReady` and Home Assistant will retry the setup later.
|
||||||
|
If the reason for the failure is that the password is incorrect or the api key is invalid, we should raise `ConfigEntryAuthFailed` and Home Assistant will ask the user to reauthenticate (if the reauthentication flow is implemented).
|
||||||
|
If we don't expect the integration to work in the foreseeable future, we should raise `ConfigEntryError`.
|
||||||
|
|
||||||
|
`__init__.py`:
|
||||||
|
```python {6-13} showLineNumbers
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: MyIntegrationConfigEntry) -> bool:
|
||||||
|
"""Set up my integration from a config entry."""
|
||||||
|
|
||||||
|
client = MyClient(entry.data[CONF_HOST])
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.async_setup()
|
||||||
|
except OfflineException:
|
||||||
|
raise ConfigEntryNotReady("Device is offline")
|
||||||
|
except InvalidAuthException:
|
||||||
|
raise ConfigEntryAuthFailed("Invalid authentication")
|
||||||
|
except AccountClosedException:
|
||||||
|
raise ConfigEntryError("Account closed")
|
||||||
|
|
||||||
|
entry.runtime_data = client
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config entries and their lifecycle can be found in the [config entry documentation](../../../config_entries_index).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
26
docs/core/integration-quality-scale/rules/test-coverage.md
Normal file
26
docs/core/integration-quality-scale/rules/test-coverage.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Above 95% test coverage for all integration modules"
|
||||||
|
related_rules:
|
||||||
|
- config-flow-test-coverage
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Since we support a lot of different integrations, we don't have every device or service available for hands-on testing.
|
||||||
|
To make sure that we don't break anything, when accepting a code change, we need to have a good test coverage for all integration modules.
|
||||||
|
This prevents the introduction of bugs and regressions.
|
||||||
|
|
||||||
|
It also allows new developers to understand the codebase and make changes without breaking any existing use case.
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
For more information about testing and how to calculate test coverage, see the [Testing your code](../../../development_testing) page.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
118
docs/core/integration-quality-scale/rules/unique-config-entry.md
Normal file
118
docs/core/integration-quality-scale/rules/unique-config-entry.md
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: "Don't allow the same device or service to be able to be set up twice"
|
||||||
|
related_rules:
|
||||||
|
- config-flow
|
||||||
|
- test-before-configure
|
||||||
|
- config-flow-test-coverage
|
||||||
|
- discovery
|
||||||
|
- reauthentication-flow
|
||||||
|
- reconfiguration-flow
|
||||||
|
---
|
||||||
|
import RelatedRules from './_includes/related_rules.jsx'
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
|
||||||
|
Since integrations are easy to set up with the UI, the user could accidentally set up the same device or service twice.
|
||||||
|
This can lead to duplicated devices and entities with unique identifiers colliding, which has negative side effects.
|
||||||
|
Any discovery flow must also ensure that a config entry is uniquely identifiable, as otherwise, it would discover devices already set up.
|
||||||
|
|
||||||
|
To prevent this, we need to ensure that the user can only set up a device or service once.
|
||||||
|
|
||||||
|
## Example implementation
|
||||||
|
|
||||||
|
There are 2 common ways an integration checks if it has already been set up.
|
||||||
|
The first way is by assigning a `unique_id` to the configuration entry.
|
||||||
|
The second way is by checking if pieces of the data in the configuration entry are unique.
|
||||||
|
|
||||||
|
The following examples show how to implement these checks in a config flow.
|
||||||
|
|
||||||
|
### Unique identifier
|
||||||
|
|
||||||
|
The first way is by assigning a `unique_id` to the configuration entry.
|
||||||
|
This unique ID is unique per integration domain, so another integration can use the same unique ID without problems.
|
||||||
|
Below is an example of a config flow that fetches the `unique_id` for the entered configuration with the client and checks if the `unique_id` already exists.
|
||||||
|
If it does, the flow will abort and show an error message to the user.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python {16-17} showLineNumbers
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
client = MyClient(user_input[CONF_HOST])
|
||||||
|
try:
|
||||||
|
identifier = await client.get_identifier()
|
||||||
|
except MyException:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(identifier)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): TextSelector()}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unique data
|
||||||
|
|
||||||
|
The second way is by checking if pieces of the data in the configuration entry are unique.
|
||||||
|
In the following example, the user fills in a host and a password.
|
||||||
|
If a configuration entry already exists for the same host, the flow will abort and show an error message to the user.
|
||||||
|
|
||||||
|
`config_flow.py`:
|
||||||
|
```python
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input:
|
||||||
|
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||||
|
client = MyClient(user_input[CONF_HOST], user_input[CONF_PASSWORD])
|
||||||
|
try:
|
||||||
|
await client.get_data()
|
||||||
|
except MyException:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="MyIntegration",
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): TextSelector(),
|
||||||
|
vol.Required(CONF_PASSWORD): TextSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Additional resources
|
||||||
|
|
||||||
|
More information about config flows can be found in the [config flow documentation](../../../config_entries_config_flow_handler).
|
||||||
|
More information about the requirements for a unique identifier can be found in the [documentation](../../../entity_registry_index#unique-id-requirements).
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
There are no exceptions to this rule.
|
||||||
|
|
||||||
|
## Related rules
|
||||||
|
|
||||||
|
<RelatedRules relatedRules={frontMatter.related_rules}></RelatedRules>
|
@ -1,68 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Integration quality scale"
|
|
||||||
---
|
|
||||||
|
|
||||||
The Integration Quality Scale scores each integration based on the code quality and user experience. Each level of the quality scale consists of a list of requirements. If an integration matches all requirements, it's considered to have reached that level.
|
|
||||||
|
|
||||||
:::info
|
|
||||||
Suggestions for changes can be done by creating an issue in the [architecture repo](https://github.com/home-assistant/architecture/discussions).
|
|
||||||
:::
|
|
||||||
|
|
||||||
## No score
|
|
||||||
|
|
||||||
This integration passes the bare minimum requirements to become part of the index.
|
|
||||||
|
|
||||||
- Satisfy all requirements for [creating components](creating_component_code_review.md) and [creating platforms](creating_platform_code_review.md).
|
|
||||||
- Configurable via `configuration.yaml`
|
|
||||||
|
|
||||||
## Silver 🥈
|
|
||||||
|
|
||||||
This integration is able to cope when things go wrong. It will not print any exceptions nor will it fill the log with retry attempts.
|
|
||||||
|
|
||||||
- Satisfying all No score level requirements.
|
|
||||||
- Connection/configuration is handled via a component.
|
|
||||||
- Set an appropriate `SCAN_INTERVAL` (if a polling integration)
|
|
||||||
- Raise [`PlatformNotReady`](integration_setup_failures.md#integrations-using-async_setup_platform) if unable to connect during platform setup (if appropriate)
|
|
||||||
- Handles expiration of auth credentials. Refresh if possible or print correct error and fail setup. If based on a config entry, should trigger a new config entry flow to re-authorize. ([docs](config_entries_config_flow_handler.md#reauthentication))
|
|
||||||
- Handles internet unavailable. Log a warning once when unavailable, log once when reconnected.
|
|
||||||
- Handles device/service unavailable. Log a warning once when unavailable, log once when reconnected.
|
|
||||||
- Operations like service action calls and entity methods (e.g. *Set HVAC Mode*) have proper exception handling. Raise `ServiceValidationError` on invalid user input and raise `HomeAssistantError` for other failures such as a problem communicating with a device. [Read more](/docs/core/platform/raising_exceptions) about raising exceptions.
|
|
||||||
- Set `available` property to `False` if appropriate ([docs](core/entity.md#generic-properties))
|
|
||||||
- Entities have unique ID (if available) ([docs](entity_registry_index.md#unique-id-requirements))
|
|
||||||
|
|
||||||
## Gold 🥇
|
|
||||||
|
|
||||||
This is a solid integration that is able to survive poor conditions and can be configured via the user interface.
|
|
||||||
|
|
||||||
- Satisfying all Silver level requirements.
|
|
||||||
- Configurable via config entries.
|
|
||||||
- Don't allow configuring already configured device/service (example: no 2 entries for same hub)
|
|
||||||
- Discoverable (if available)
|
|
||||||
- Set unique ID in config flow (if available)
|
|
||||||
- Raise [`ConfigEntryNotReady`](integration_setup_failures.md#integrations-using-async_setup_entry) if unable to connect during entry setup (if appropriate)
|
|
||||||
- Entities have device info (if available) ([docs](device_registry_index.md#defining-devices))
|
|
||||||
- Tests
|
|
||||||
- Full test coverage for the config flow
|
|
||||||
- Above average test coverage for all integration modules
|
|
||||||
- Tests for fetching data from the integration and controlling it ([docs](development_testing.md))
|
|
||||||
- Has a code owner ([docs](creating_integration_manifest.md#code-owners))
|
|
||||||
- Entities only subscribe to updates inside `async_added_to_hass` and unsubscribe inside `async_will_remove_from_hass` ([docs](core/entity.md#lifecycle-hooks))
|
|
||||||
- Entities have correct device classes where appropriate ([docs](core/entity.md#generic-properties))
|
|
||||||
- Supports entities being disabled and leverages `Entity.entity_registry_enabled_default` to disable less popular entities ([docs](core/entity.md#advanced-properties))
|
|
||||||
- If the device/service API can remove entities, the integration should make sure to clean up the entity and device registry.
|
|
||||||
- When communicating with a device or service, the integration implements the diagnostics platform which redacts sensitive information.
|
|
||||||
|
|
||||||
## Platinum 🏆
|
|
||||||
|
|
||||||
Best of the best. The integration is completely async, meaning it's super fast. Integrations that reach platinum level will require approval by the code owner for each PR.
|
|
||||||
|
|
||||||
- Satisfying all Gold level requirements.
|
|
||||||
- Set appropriate `PARALLEL_UPDATES` constant ([docs](integration_fetching_data.md#request-parallelism))
|
|
||||||
- Support config entry unloading (called when config entry is removed)
|
|
||||||
- Integration + dependency are async ([docs](asyncio_working_with_async.md))
|
|
||||||
- Uses aiohttp or httpx and allows passing in websession (if making HTTP requests)
|
|
||||||
- [Handles expired credentials](integration_setup_failures.md#handling-expired-credentials) (if appropriate)
|
|
||||||
|
|
||||||
## Internal 🏠
|
|
||||||
|
|
||||||
Integrations which are part of Home Assistant are not rated but marked as **internal**.
|
|
24
sidebars.js
24
sidebars.js
@ -5,6 +5,16 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const iqs_rules_by_tier = require('./docs/core/integration-quality-scale/_includes/tiers.json');
|
||||||
|
const iqs_rules = Object.values(iqs_rules_by_tier).flat().map((rule) => {
|
||||||
|
if (typeof rule === "string") {
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
return rule.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Addons: [
|
Addons: [
|
||||||
"add-ons",
|
"add-ons",
|
||||||
@ -134,9 +144,21 @@ module.exports = {
|
|||||||
"development_checklist",
|
"development_checklist",
|
||||||
"creating_component_code_review",
|
"creating_component_code_review",
|
||||||
"creating_platform_code_review",
|
"creating_platform_code_review",
|
||||||
"integration_quality_scale_index",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "Integration Quality Scale",
|
||||||
|
link: {type: 'doc', id: 'core/integration-quality-scale/index'},
|
||||||
|
items: [
|
||||||
|
{type: 'doc', id: 'core/integration-quality-scale/checklist'},
|
||||||
|
{type: 'category', label: 'Rules', items: iqs_rules.map(rule => ({
|
||||||
|
type: 'doc',
|
||||||
|
id: `core/integration-quality-scale/rules/${rule.toLowerCase()}`
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "The `hass` object",
|
label: "The `hass` object",
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
/docs/hassio_addon_tutorial /docs/add-ons/tutorial
|
/docs/hassio_addon_tutorial /docs/add-ons/tutorial
|
||||||
/docs/hassio_debugging /docs/supervisor/debugging
|
/docs/hassio_debugging /docs/supervisor/debugging
|
||||||
/docs/hassio_hass /docs/supervisor/developing
|
/docs/hassio_hass /docs/supervisor/developing
|
||||||
|
/docs/integration_quality_scale_index /docs/core/integration-quality-scale
|
||||||
/docs/internationalization_backend_localization /docs/internationalization/core
|
/docs/internationalization_backend_localization /docs/internationalization/core
|
||||||
/docs/internationalization_custom_component_localization /docs/internationalization/custom_integration
|
/docs/internationalization_custom_component_localization /docs/internationalization/custom_integration
|
||||||
/docs/internationalization_index /docs/internationalization
|
/docs/internationalization_index /docs/internationalization
|
||||||
|
Loading…
x
Reference in New Issue
Block a user