Integrations v2.1: Virtual integrations (#80613)

This commit is contained in:
Franck Nijhof 2022-10-21 05:09:06 +02:00 committed by GitHub
parent 6c23de94e1
commit bb287dd0ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 2968 additions and 2720 deletions

View File

@ -0,0 +1,6 @@
{
"domain": "3_day_blinds",
"name": "3 Day Blinds",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "amp_motorization",
"name": "AMP Motorization",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "august_ble",
"name": "August Bluetooth",
"integration_type": "virtual",
"supported_by": "yalexs_ble"
}

View File

@ -0,0 +1,6 @@
{
"domain": "bliss_automation",
"name": "Bliss Automation",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "bloc_blinds",
"name": "Bloc Blinds",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "brel_home",
"name": "Brel Home",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "bswitch",
"name": "BSwitch",
"integration_type": "virtual",
"supported_by": "switchbee"
}

View File

@ -0,0 +1,6 @@
{
"domain": "bticino",
"name": "BTicino",
"integration_type": "virtual",
"supported_by": "netatmo"
}

View File

@ -0,0 +1,6 @@
{
"domain": "bubendorff",
"name": "Bubendorff",
"integration_type": "virtual",
"supported_by": "netatmo"
}

View File

@ -0,0 +1,6 @@
{
"domain": "cozytouch",
"name": "Atlantic Cozytouch",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -0,0 +1,6 @@
{
"domain": "dacia",
"name": "Dacia",
"integration_type": "virtual",
"supported_by": "renault"
}

View File

@ -56,8 +56,5 @@
} }
], ],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["denonavr"], "loggers": ["denonavr"]
"supported_brands": {
"marantz": "Marantz"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "diaz",
"name": "Diaz",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "digital_loggers",
"name": "Digital Loggers",
"integration_type": "virtual",
"supported_by": "wemo"
}

View File

@ -0,0 +1,6 @@
{
"domain": "dooya",
"name": "Dooya",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "flexom",
"name": "Bouygues Flexom",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -0,0 +1,6 @@
{
"domain": "gaviota",
"name": "Gaviota",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -7,8 +7,5 @@
"dependencies": ["network"], "dependencies": ["network"],
"codeowners": ["@cmroche"], "codeowners": ["@cmroche"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["greeclimate"], "loggers": ["greeclimate"]
"supported_brands": {
"heiwa": "Heiwa"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "havana_shade",
"name": "Havana Shade",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "heiwa",
"name": "Heiwa",
"integration_type": "virtual",
"supported_by": "gree"
}

View File

@ -0,0 +1,6 @@
{
"domain": "hi_kumo",
"name": "Hitachi Hi Kumo",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -17,8 +17,5 @@
], ],
"zeroconf": ["_powerview._tcp.local."], "zeroconf": ["_powerview._tcp.local."],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiopvapi"], "loggers": ["aiopvapi"]
"supported_brands": {
"luxaflex": "Luxaflex"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "hurrican_shutters_wholesale",
"name": "Hurrican Shutters Wholesale",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -13,8 +13,5 @@
"requirements": ["inkbird-ble==0.5.5"], "requirements": ["inkbird-ble==0.5.5"],
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"iot_class": "local_push", "iot_class": "local_push"
"supported_brands": {
"nutrichef": "Nutrichef"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "inspired_shades",
"name": "Inspired Shades",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "ismartwindow",
"name": "iSmartWindow",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "legrand",
"name": "Legrand",
"integration_type": "virtual",
"supported_by": "netatmo"
}

View File

@ -0,0 +1,6 @@
{
"domain": "luxaflex",
"name": "Luxaflex",
"integration_type": "virtual",
"supported_by": "hunterdouglas_powerview"
}

View File

@ -0,0 +1,6 @@
{
"domain": "marantz",
"name": "Marantz",
"integration_type": "virtual",
"supported_by": "denonavr"
}

View File

@ -0,0 +1,6 @@
{
"domain": "martec",
"name": "Martec",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -19,25 +19,5 @@
], ],
"codeowners": ["@starkillerOG"], "codeowners": ["@starkillerOG"],
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["motionblinds"], "loggers": ["motionblinds"]
"supported_brands": {
"amp_motorization": "AMP Motorization",
"bliss_automation": "Bliss Automation",
"bloc_blinds": "Bloc Blinds",
"brel_home": "Brel Home",
"3_day_blinds": "3 Day Blinds",
"diaz": "Diaz",
"dooya": "Dooya",
"gaviota": "Gaviota",
"havana_shade": "Havana Shade",
"hurrican_shutters_wholesale": "Hurrican Shutters Wholesale",
"inspired_shades": "Inspired Shades",
"ismartwindow": "iSmartWindow",
"martec": "Martec",
"raven_rock_mfg": "Raven Rock MFG",
"screenaway": "ScreenAway",
"smart_blinds": "Smart Blinds",
"smart_home": "Smart Home",
"uprise_smart_shades": "Uprise Smart Shades"
}
} }

View File

@ -11,11 +11,5 @@
"models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"]
}, },
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyatmo"], "loggers": ["pyatmo"]
"supported_brands": {
"legrand": "Legrand",
"bubendorff": "Bubendorff",
"smarther": "Smarther",
"bticino": "BTicino"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "nexity",
"name": "Nexity Eugénie",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -0,0 +1,6 @@
{
"domain": "nutrichef",
"name": "Nutrichef",
"integration_type": "virtual",
"supported_by": "inkbird"
}

View File

@ -18,13 +18,5 @@
], ],
"codeowners": ["@imicknl", "@vlebourl", "@tetienne"], "codeowners": ["@imicknl", "@vlebourl", "@tetienne"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"]
"supported_brands": {
"cozytouch": "Atlantic Cozytouch",
"flexom": "Bouygues Flexom",
"hi_kumo": "Hitachi Hi Kumo",
"nexity": "Nexity Eugénie",
"rexel": "Rexel Energeasy Connect",
"somfy": "Somfy"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "pcs_lighting",
"name": "PCS Lighting",
"integration_type": "virtual",
"supported_by": "upb"
}

View File

@ -0,0 +1,6 @@
{
"domain": "raven_rock_mfg",
"name": "Raven Rock MFG",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -6,6 +6,5 @@
"requirements": ["renault-api==0.1.11"], "requirements": ["renault-api==0.1.11"],
"codeowners": ["@epenet"], "codeowners": ["@epenet"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["renault_api"], "loggers": ["renault_api"]
"supported_brands": { "dacia": "Dacia" }
} }

View File

@ -0,0 +1,6 @@
{
"domain": "rexel",
"name": "Rexel Energeasy Connect",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -0,0 +1,6 @@
{
"domain": "roborock",
"name": "Roborock",
"integration_type": "virtual",
"supported_by": "xiaomi_miio"
}

View File

@ -0,0 +1,6 @@
{
"domain": "screenaway",
"name": "ScreenAway",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "sensorblue",
"name": "SensorBlue",
"integration_type": "virtual",
"supported_by": "thermobeacon"
}

View File

@ -0,0 +1,6 @@
{
"domain": "simply_automated",
"name": "Simply Automated",
"integration_type": "virtual",
"supported_by": "upb"
}

View File

@ -0,0 +1,6 @@
{
"domain": "smart_blinds",
"name": "Smart Blinds",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "smart_home",
"name": "Smart Home",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -0,0 +1,6 @@
{
"domain": "smarther",
"name": "Smarther",
"integration_type": "virtual",
"supported_by": "netatmo"
}

View File

@ -0,0 +1,6 @@
{
"domain": "somfy",
"name": "Somfy",
"integration_type": "virtual",
"supported_by": "overkiz"
}

View File

@ -5,8 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbee", "documentation": "https://www.home-assistant.io/integrations/switchbee",
"requirements": ["pyswitchbee==1.5.5"], "requirements": ["pyswitchbee==1.5.5"],
"codeowners": ["@jafar-atili"], "codeowners": ["@jafar-atili"],
"iot_class": "local_polling", "iot_class": "local_polling"
"supported_brands": {
"bswitch": "BSwitch"
}
} }

View File

@ -27,9 +27,5 @@
"requirements": ["thermobeacon-ble==0.3.2"], "requirements": ["thermobeacon-ble==0.3.2"],
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"iot_class": "local_push", "iot_class": "local_push"
"supported_brands": {
"thermoplus": "ThermoPlus",
"sensorblue": "SensorBlue"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "thermoplus",
"name": "ThermoPlus",
"integration_type": "virtual",
"supported_by": "thermobeacon"
}

View File

@ -6,9 +6,5 @@
"codeowners": ["@gwww"], "codeowners": ["@gwww"],
"config_flow": true, "config_flow": true,
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["upb_lib"], "loggers": ["upb_lib"]
"supported_brands": {
"pcs_lighting": "PCS Lighting",
"simply_automated": "Simply Automated"
}
} }

View File

@ -0,0 +1,6 @@
{
"domain": "uprise_smart_shades",
"name": "Uprise Smart Shades",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -21,7 +21,6 @@ from homeassistant.exceptions import (
TemplateError, TemplateError,
Unauthorized, Unauthorized,
) )
from homeassistant.generated import supported_brands
from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers import config_validation as cv, entity, template
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
@ -74,7 +73,6 @@ def async_register_commands(
async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_unsubscribe_events)
async_reg(hass, handle_validate_config) async_reg(hass, handle_validate_config)
async_reg(hass, handle_subscribe_entities) async_reg(hass, handle_subscribe_entities)
async_reg(hass, handle_supported_brands)
async_reg(hass, handle_supported_features) async_reg(hass, handle_supported_features)
async_reg(hass, handle_integration_descriptions) async_reg(hass, handle_integration_descriptions)
@ -705,31 +703,6 @@ async def handle_validate_config(
connection.send_result(msg["id"], result) connection.send_result(msg["id"], result)
@decorators.websocket_command(
{
vol.Required("type"): "supported_brands",
}
)
@decorators.async_response
async def handle_supported_brands(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle supported brands command."""
data = {}
ints_or_excs = await async_get_integrations(
hass, supported_brands.HAS_SUPPORTED_BRANDS
)
for int_or_exc in ints_or_excs.values():
if isinstance(int_or_exc, Exception):
raise int_or_exc
# Happens if a custom component without supported brands overrides a built-in one with supported brands
if "supported_brands" not in int_or_exc.manifest:
continue
data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"]
connection.send_result(msg["id"], data)
@callback @callback
@decorators.websocket_command( @decorators.websocket_command(
{ {

View File

@ -14,8 +14,5 @@
}, },
"codeowners": ["@esev"], "codeowners": ["@esev"],
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pywemo"], "loggers": ["pywemo"]
"supported_brands": {
"digital_loggers": "Digital Loggers"
}
} }

View File

@ -7,8 +7,5 @@
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."], "zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["micloud", "miio"], "loggers": ["micloud", "miio"]
"supported_brands": {
"roborock": "Roborock"
}
} }

View File

@ -12,8 +12,5 @@
"service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb"
} }
], ],
"iot_class": "local_push", "iot_class": "local_push"
"supported_brands": {
"august_ble": "August Bluetooth"
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
"""Automatically generated by hassfest.
To update, run python3 -m script.hassfest
"""
HAS_SUPPORTED_BRANDS = [
"denonavr",
"gree",
"hunterdouglas_powerview",
"inkbird",
"motion_blinds",
"netatmo",
"overkiz",
"renault",
"switchbee",
"thermobeacon",
"upb",
"wemo",
"xiaomi_miio",
"yalexs_ble",
]

View File

@ -152,7 +152,6 @@ class Manifest(TypedDict, total=False):
version: str version: str
codeowners: list[str] codeowners: list[str]
loggers: list[str] loggers: list[str]
supported_brands: dict[str, str]
def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:

View File

@ -20,7 +20,6 @@ from . import (
requirements, requirements,
services, services,
ssdp, ssdp,
supported_brands,
translations, translations,
usb, usb,
zeroconf, zeroconf,
@ -39,7 +38,6 @@ INTEGRATION_PLUGINS = [
requirements, requirements,
services, services,
ssdp, ssdp,
supported_brands,
translations, translations,
usb, usb,
zeroconf, zeroconf,

View File

@ -47,7 +47,10 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config):
for domain in sorted(integrations): for domain in sorted(integrations):
integration = integrations[domain] integration = integrations[domain]
if not integration.manifest: if (
not integration.manifest
or integration.manifest.get("integration_type") == "virtual"
):
continue continue
codeowners = integration.manifest["codeowners"] codeowners = integration.manifest["codeowners"]

View File

@ -171,14 +171,24 @@ def _generate_integrations(
integration = integrations[domain] integration = integrations[domain]
if integration.integration_type in ("entity", "system"): if integration.integration_type in ("entity", "system"):
continue continue
metadata["config_flow"] = integration.config_flow
metadata["iot_class"] = integration.iot_class
metadata["integration_type"] = integration.integration_type
if integration.translated_name: if integration.translated_name:
result["translated_name"].add(domain) result["translated_name"].add(domain)
else: else:
metadata["name"] = integration.name metadata["name"] = integration.name
metadata["integration_type"] = integration.integration_type
if integration.integration_type == "virtual":
if integration.supported_by:
metadata["supported_by"] = integration.supported_by
if integration.iot_standard:
metadata["iot_standard"] = integration.iot_standard
else:
metadata["config_flow"] = integration.config_flow
if integration.iot_class:
metadata["iot_class"] = integration.iot_class
if integration.integration_type == "helper": if integration.integration_type == "helper":
result["helper"][domain] = metadata result["helper"][domain] = metadata
else: else:

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from awesomeversion import ( from awesomeversion import (
@ -158,7 +159,7 @@ def verify_wildcard(value: str):
return value return value
MANIFEST_SCHEMA = vol.Schema( INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
{ {
vol.Required("domain"): str, vol.Required("domain"): str,
vol.Required("name"): str, vol.Required("name"): str,
@ -254,14 +255,32 @@ MANIFEST_SCHEMA = vol.Schema(
vol.Optional("loggers"): [str], vol.Optional("loggers"): [str],
vol.Optional("disabled"): str, vol.Optional("disabled"): str,
vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES),
vol.Optional("supported_brands"): vol.Schema({str: str}),
} }
) )
CUSTOM_INTEGRATION_MANIFEST_SCHEMA = MANIFEST_SCHEMA.extend( VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
{
vol.Required("domain"): str,
vol.Required("name"): str,
vol.Required("integration_type"): "virtual",
vol.Exclusive("iot_standard", "virtual_integration"): vol.Any(
"homekit", "zigbee", "zwave"
),
vol.Exclusive("supported_by", "virtual_integration"): str,
}
)
def manifest_schema(value: dict[str, Any]) -> vol.Schema:
"""Validate integration manifest."""
if value.get("integration_type") == "virtual":
return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value)
return INTEGRATION_MANIFEST_SCHEMA(value)
CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend(
{ {
vol.Optional("version"): vol.All(str, verify_version), vol.Optional("version"): vol.All(str, verify_version),
vol.Remove("supported_brands"): dict,
} }
) )
@ -284,7 +303,7 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
try: try:
if integration.core: if integration.core:
MANIFEST_SCHEMA(integration.manifest) manifest_schema(integration.manifest)
else: else:
CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
except vol.Invalid as err: except vol.Invalid as err:
@ -312,15 +331,19 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
if ( if (
integration.manifest["domain"] not in NO_IOT_CLASS integration.manifest["domain"] not in NO_IOT_CLASS
and "iot_class" not in integration.manifest and "iot_class" not in integration.manifest
and integration.manifest.get("integration_type") != "virtual"
): ):
integration.add_error("manifest", "Domain is missing an IoT Class") integration.add_error("manifest", "Domain is missing an IoT Class")
for domain, _name in integration.manifest.get("supported_brands", {}).items(): if (
if (core_components_dir / domain).exists(): integration.manifest.get("integration_type") == "virtual"
integration.add_warning( and (supported_by := integration.manifest.get("supported_by"))
"manifest", and not (core_components_dir / supported_by).exists()
f"Supported brand domain {domain} collides with built-in core integration", ):
) integration.add_error(
"manifest",
"Virtual integration points to non-existing supported_by integration",
)
if not integration.core: if not integration.core:
validate_version(integration) validate_version(integration)

View File

@ -109,11 +109,12 @@ class Integration:
continue continue
init = fil / "__init__.py" init = fil / "__init__.py"
if not init.exists(): manifest = fil / "manifest.json"
if not init.exists() and not manifest.exists():
print( print(
f"Warning: {init} missing, skipping directory. " f"Warning: {init} and manifest.json missing, "
"If this is your development environment, " "skipping directory. If this is your development "
"you can safely delete this folder." "environment, you can safely delete this folder."
) )
continue continue
@ -170,9 +171,9 @@ class Integration:
return self.manifest.get("dependencies", []) return self.manifest.get("dependencies", [])
@property @property
def supported_brands(self) -> dict[str]: def supported_by(self) -> str:
"""Return dict of supported brands.""" """Return the integration supported by this virtual integration."""
return self.manifest.get("supported_brands", {}) return self.manifest.get("supported_by", {})
@property @property
def integration_type(self) -> str: def integration_type(self) -> str:
@ -184,6 +185,11 @@ class Integration:
"""Return the integration IoT Class.""" """Return the integration IoT Class."""
return self.manifest.get("iot_class") return self.manifest.get("iot_class")
@property
def iot_standard(self) -> str:
"""Return the IoT standard supported by this virtual integration."""
return self.manifest.get("iot_standard", {})
def add_error(self, *args: Any, **kwargs: Any) -> None: def add_error(self, *args: Any, **kwargs: Any) -> None:
"""Add an error.""" """Add an error."""
self.errors.append(Error(*args, **kwargs)) self.errors.append(Error(*args, **kwargs))

View File

@ -1,54 +0,0 @@
"""Generate supported_brands data."""
from __future__ import annotations
import black
from .model import Config, Integration
from .serializer import to_string
BASE = """
\"\"\"Automatically generated by hassfest.
To update, run python3 -m script.hassfest
\"\"\"
HAS_SUPPORTED_BRANDS = {}
""".strip()
def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str:
"""Validate and generate supported_brands data."""
brands = [
domain
for domain, integration in sorted(integrations.items())
if integration.supported_brands
]
return black.format_str(BASE.format(to_string(brands)), mode=black.Mode())
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate supported_brands data."""
supported_brands_path = config.root / "homeassistant/generated/supported_brands.py"
config.cache["supported_brands"] = content = generate_and_validate(
integrations, config
)
if config.specific_integrations:
return
if supported_brands_path.read_text(encoding="utf-8") != content:
config.add_error(
"supported_brands",
"File supported_brands.py is not up to date. Run python3 -m script.hassfest",
fixable=True,
)
def generate(integrations: dict[str, Integration], config: Config):
"""Generate supported_brands data."""
supported_brands_path = config.root / "homeassistant/generated/supported_brands.py"
supported_brands_path.write_text(
f"{config.cache['supported_brands']}", encoding="utf-8"
)

View File

@ -23,13 +23,7 @@ from homeassistant.helpers.json import json_loads
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
from homeassistant.setup import DATA_SETUP_TIME, async_setup_component from homeassistant.setup import DATA_SETUP_TIME, async_setup_component
from tests.common import ( from tests.common import MockEntity, MockEntityPlatform, async_mock_service
MockEntity,
MockEntityPlatform,
MockModule,
async_mock_service,
mock_integration,
)
STATE_KEY_SHORT_NAMES = { STATE_KEY_SHORT_NAMES = {
"entity_id": "e", "entity_id": "e",
@ -1794,45 +1788,6 @@ async def test_validate_config_invalid(websocket_client, key, config, error):
assert msg["result"] == {key: {"valid": False, "error": error}} assert msg["result"] == {key: {"valid": False, "error": error}}
async def test_supported_brands(hass, websocket_client):
"""Test supported brands."""
# Custom components without supported brands that override a built-in component with
# supported brand will still be listed in HAS_SUPPORTED_BRANDS and should be ignored.
mock_integration(
hass,
MockModule("override_without_brands"),
)
mock_integration(
hass,
MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}),
)
mock_integration(
hass,
MockModule(
"abcd", partial_manifest={"supported_brands": {"something": "Something"}}
),
)
with patch(
"homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS",
("abcd", "test", "override_without_brands"),
):
await websocket_client.send_json({"id": 7, "type": "supported_brands"})
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"] == {
"abcd": {
"something": "Something",
},
"test": {
"hello": "World",
},
}
async def test_message_coalescing(hass, websocket_client, hass_admin_user): async def test_message_coalescing(hass, websocket_client, hass_admin_user):
"""Test enabling message coalescing.""" """Test enabling message coalescing."""
await websocket_client.send_json( await websocket_client.send_json(