-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Wyoming satellite #104759
Add Wyoming satellite #104759
Changes from all commits
992713e
6e07329
ca5d347
3bc8392
6a8e04a
90d2d53
6556b5b
82d94d2
65c9636
a8b6a50
822f6fc
b0731ea
2ca80a1
454640d
a50e9db
3332114
c4bf29d
19fe293
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,17 +4,26 @@ | |||||
import logging | ||||||
|
||||||
from homeassistant.config_entries import ConfigEntry | ||||||
from homeassistant.const import Platform | ||||||
from homeassistant.core import HomeAssistant | ||||||
from homeassistant.exceptions import ConfigEntryNotReady | ||||||
from homeassistant.helpers import device_registry as dr | ||||||
|
||||||
from .const import ATTR_SPEAKER, DOMAIN | ||||||
from .data import WyomingService | ||||||
from .devices import SatelliteDevice | ||||||
from .models import DomainDataItem | ||||||
from .satellite import WyomingSatellite | ||||||
|
||||||
_LOGGER = logging.getLogger(__name__) | ||||||
|
||||||
SATELLITE_PLATFORMS = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SWITCH] | ||||||
|
||||||
__all__ = [ | ||||||
"ATTR_SPEAKER", | ||||||
"DOMAIN", | ||||||
"async_setup_entry", | ||||||
"async_unload_entry", | ||||||
] | ||||||
|
||||||
|
||||||
|
@@ -25,24 +34,72 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | |||||
if service is None: | ||||||
raise ConfigEntryNotReady("Unable to connect") | ||||||
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = service | ||||||
item = DomainDataItem(service=service) | ||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = item | ||||||
|
||||||
await hass.config_entries.async_forward_entry_setups( | ||||||
entry, | ||||||
service.platforms, | ||||||
) | ||||||
await hass.config_entries.async_forward_entry_setups(entry, service.platforms) | ||||||
entry.async_on_unload(entry.add_update_listener(update_listener)) | ||||||
|
||||||
if (satellite_info := service.info.satellite) is not None: | ||||||
# Create satellite device, etc. | ||||||
item.satellite = _make_satellite(hass, entry, service) | ||||||
|
||||||
# Set up satellite sensors, switches, etc. | ||||||
await hass.config_entries.async_forward_entry_setups(entry, SATELLITE_PLATFORMS) | ||||||
|
||||||
# Start satellite communication | ||||||
entry.async_create_background_task( | ||||||
hass, | ||||||
item.satellite.run(), | ||||||
f"Satellite {satellite_info.name}", | ||||||
) | ||||||
|
||||||
entry.async_on_unload(item.satellite.stop) | ||||||
|
||||||
return True | ||||||
|
||||||
|
||||||
def _make_satellite( | ||||||
hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService | ||||||
) -> WyomingSatellite: | ||||||
"""Create Wyoming satellite/device from config entry and Wyoming service.""" | ||||||
satellite_info = service.info.satellite | ||||||
assert satellite_info is not None | ||||||
|
||||||
dev_reg = dr.async_get(hass) | ||||||
|
||||||
# Use config entry id since only one satellite per entry is supported | ||||||
satellite_id = config_entry.entry_id | ||||||
|
||||||
device = dev_reg.async_get_or_create( | ||||||
config_entry_id=config_entry.entry_id, | ||||||
identifiers={(DOMAIN, satellite_id)}, | ||||||
name=satellite_info.name, | ||||||
suggested_area=satellite_info.area, | ||||||
) | ||||||
|
||||||
satellite_device = SatelliteDevice( | ||||||
satellite_id=satellite_id, | ||||||
device_id=device.id, | ||||||
) | ||||||
|
||||||
return WyomingSatellite(hass, service, satellite_device) | ||||||
|
||||||
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing return value typing. |
||||||
"""Handle options update.""" | ||||||
await hass.config_entries.async_reload(entry.entry_id) | ||||||
|
||||||
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||
"""Unload Wyoming.""" | ||||||
service: WyomingService = hass.data[DOMAIN][entry.entry_id] | ||||||
item: DomainDataItem = hass.data[DOMAIN][entry.entry_id] | ||||||
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms( | ||||||
entry, | ||||||
service.platforms, | ||||||
) | ||||||
platforms = list(item.service.platforms) | ||||||
if item.satellite is not None: | ||||||
platforms += SATELLITE_PLATFORMS | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, I think this suggestion isn't an improvement... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent is to not change the original variable anymore |
||||||
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) | ||||||
if unload_ok: | ||||||
del hass.data[DOMAIN][entry.entry_id] | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
"""Binary sensor for Wyoming.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
from homeassistant.components.binary_sensor import ( | ||
BinarySensorEntity, | ||
BinarySensorEntityDescription, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from .const import DOMAIN | ||
from .entity import WyomingSatelliteEntity | ||
|
||
if TYPE_CHECKING: | ||
from .models import DomainDataItem | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Set up binary sensor entities.""" | ||
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id] | ||
|
||
# Setup is only forwarded for satellites | ||
assert item.satellite is not None | ||
|
||
async_add_entities([WyomingSatelliteAssistInProgress(item.satellite.device)]) | ||
|
||
|
||
class WyomingSatelliteAssistInProgress(WyomingSatelliteEntity, BinarySensorEntity): | ||
"""Entity to represent Assist is in progress for satellite.""" | ||
|
||
entity_description = BinarySensorEntityDescription( | ||
key="assist_in_progress", | ||
translation_key="assist_in_progress", | ||
) | ||
_attr_is_on = False | ||
|
||
async def async_added_to_hass(self) -> None: | ||
"""Call when entity about to be added to hass.""" | ||
await super().async_added_to_hass() | ||
|
||
self._device.set_is_active_listener(self._is_active_changed) | ||
|
||
@callback | ||
def _is_active_changed(self) -> None: | ||
"""Call when active state changed.""" | ||
self._attr_is_on = self._device.is_active | ||
self.async_write_ha_state() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,22 @@ | ||
"""Config flow for Wyoming integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
from urllib.parse import urlparse | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.hassio import HassioServiceInfo | ||
from homeassistant.const import CONF_HOST, CONF_PORT | ||
from homeassistant.components import hassio, zeroconf | ||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT | ||
from homeassistant.data_entry_flow import FlowResult | ||
|
||
from .const import DOMAIN | ||
from .data import WyomingService | ||
|
||
_LOGGER = logging.getLogger() | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST): str, | ||
|
@@ -27,7 +30,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | |
|
||
VERSION = 1 | ||
|
||
_hassio_discovery: HassioServiceInfo | ||
_hassio_discovery: hassio.HassioServiceInfo | ||
_service: WyomingService | None = None | ||
_name: str | None = None | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
|
@@ -50,27 +55,14 @@ async def async_step_user( | |
errors={"base": "cannot_connect"}, | ||
) | ||
|
||
# ASR = automated speech recognition (speech-to-text) | ||
asr_installed = [asr for asr in service.info.asr if asr.installed] | ||
|
||
# TTS = text-to-speech | ||
tts_installed = [tts for tts in service.info.tts if tts.installed] | ||
|
||
# wake-word-detection | ||
wake_installed = [wake for wake in service.info.wake if wake.installed] | ||
if name := service.get_name(): | ||
return self.async_create_entry(title=name, data=user_input) | ||
|
||
if asr_installed: | ||
name = asr_installed[0].name | ||
elif tts_installed: | ||
name = tts_installed[0].name | ||
elif wake_installed: | ||
name = wake_installed[0].name | ||
else: | ||
return self.async_abort(reason="no_services") | ||
|
||
return self.async_create_entry(title=name, data=user_input) | ||
return self.async_abort(reason="no_services") | ||
|
||
async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: | ||
async def async_step_hassio( | ||
self, discovery_info: hassio.HassioServiceInfo | ||
) -> FlowResult: | ||
"""Handle Supervisor add-on discovery.""" | ||
await self.async_set_unique_id(discovery_info.uuid) | ||
self._abort_if_unique_id_configured() | ||
|
@@ -93,11 +85,7 @@ async def async_step_hassio_confirm( | |
if user_input is not None: | ||
uri = urlparse(self._hassio_discovery.config["uri"]) | ||
if service := await WyomingService.create(uri.hostname, uri.port): | ||
if ( | ||
not any(asr for asr in service.info.asr if asr.installed) | ||
and not any(tts for tts in service.info.tts if tts.installed) | ||
and not any(wake for wake in service.info.wake if wake.installed) | ||
): | ||
if not service.has_services(): | ||
return self.async_abort(reason="no_services") | ||
|
||
return self.async_create_entry( | ||
|
@@ -112,3 +100,52 @@ async def async_step_hassio_confirm( | |
description_placeholders={"addon": self._hassio_discovery.name}, | ||
errors=errors, | ||
) | ||
|
||
async def async_step_zeroconf( | ||
self, discovery_info: zeroconf.ZeroconfServiceInfo | ||
) -> FlowResult: | ||
"""Handle zeroconf discovery.""" | ||
_LOGGER.debug("Discovery info: %s", discovery_info) | ||
if discovery_info.port is None: | ||
return self.async_abort(reason="no_port") | ||
|
||
service = await WyomingService.create(discovery_info.host, discovery_info.port) | ||
if (service is None) or (not (name := service.get_name())): | ||
# No supported services | ||
return self.async_abort(reason="no_services") | ||
|
||
self._name = name | ||
|
||
# Use zeroconf name + service name as unique id. | ||
# The satellite will use its own MAC as the zeroconf name by default. | ||
unique_id = f"{discovery_info.name}_{self._name}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the local variable |
||
await self.async_set_unique_id(unique_id) | ||
self._abort_if_unique_id_configured() | ||
|
||
self.context[CONF_NAME] = self._name | ||
self.context["title_placeholders"] = {"name": self._name} | ||
|
||
self._service = service | ||
return await self.async_step_zeroconf_confirm() | ||
|
||
async def async_step_zeroconf_confirm( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle a flow initiated by zeroconf.""" | ||
assert self._service is not None | ||
assert self._name is not None | ||
|
||
if user_input is None: | ||
return self.async_show_form( | ||
step_id="zeroconf_confirm", | ||
description_placeholders={"name": self._name}, | ||
errors={}, | ||
) | ||
|
||
return self.async_create_entry( | ||
title=self._name, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Define a local variable when accessing an attribute more than once. |
||
data={ | ||
CONF_HOST: self._service.host, | ||
CONF_PORT: self._service.port, | ||
}, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that it would be an error if we tried to forward the config entry more than once to a platform. There's no overlap at the moment, and I guess it would be caught in tests.