forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Starlink Integration (home-assistant#77091)
Co-authored-by: J. Nick Koston <[email protected]>
- Loading branch information
Showing
21 changed files
with
493 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
"""The Starlink integration.""" | ||
from __future__ import annotations | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_IP_ADDRESS, Platform | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .const import DOMAIN | ||
from .coordinator import StarlinkUpdateCoordinator | ||
|
||
PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Starlink from a config entry.""" | ||
coordinator = StarlinkUpdateCoordinator( | ||
hass=hass, | ||
url=entry.data[CONF_IP_ADDRESS], | ||
name=entry.title, | ||
) | ||
|
||
await coordinator.async_config_entry_first_refresh() | ||
|
||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Config flow for Starlink.""" | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from starlink_grpc import ChannelContext, GrpcError, get_id | ||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import CONF_IP_ADDRESS | ||
from homeassistant.data_entry_flow import FlowResult | ||
|
||
from .const import DOMAIN | ||
|
||
CONFIG_SCHEMA = vol.Schema( | ||
{vol.Required(CONF_IP_ADDRESS, default="192.168.100.1:9200"): str} | ||
) | ||
|
||
|
||
class StarlinkConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""The configuration flow for a Starlink system.""" | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Ask the user for a server address and a name for the system.""" | ||
errors = {} | ||
if user_input: | ||
# Input validation. If everything looks good, create the entry | ||
if uid := await self.get_device_id(url=user_input[CONF_IP_ADDRESS]): | ||
# Make sure we're not configuring the same device | ||
await self.async_set_unique_id(uid) | ||
self._abort_if_unique_id_configured() | ||
|
||
return self.async_create_entry( | ||
title="Starlink", | ||
data=user_input, | ||
) | ||
errors[CONF_IP_ADDRESS] = "cannot_connect" | ||
return self.async_show_form( | ||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors | ||
) | ||
|
||
async def get_device_id(self, url: str) -> str | None: | ||
"""Get the device UID, or None if no device exists at the given URL.""" | ||
context = ChannelContext(target=url) | ||
try: | ||
response = await self.hass.async_add_executor_job(get_id, context) | ||
except GrpcError: | ||
response = None | ||
context.close() | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for the Starlink integration.""" | ||
|
||
DOMAIN = "starlink" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""Contains the shared Coordinator for Starlink systems.""" | ||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
import async_timeout | ||
from starlink_grpc import ChannelContext, GrpcError, StatusDict, status_data | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class StarlinkUpdateCoordinator(DataUpdateCoordinator[StatusDict]): | ||
"""Coordinates updates between all Starlink sensors defined in this file.""" | ||
|
||
def __init__(self, hass: HomeAssistant, name: str, url: str) -> None: | ||
"""Initialize an UpdateCoordinator for a group of sensors.""" | ||
self.channel_context = ChannelContext(target=url) | ||
|
||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name=name, | ||
update_interval=timedelta(seconds=5), | ||
) | ||
|
||
async def _async_update_data(self) -> StatusDict: | ||
async with async_timeout.timeout(4): | ||
try: | ||
status = await self.hass.async_add_executor_job( | ||
status_data, self.channel_context | ||
) | ||
return status[0] | ||
except GrpcError as exc: | ||
raise UpdateFailed from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"""Contains base entity classes for Starlink entities.""" | ||
from __future__ import annotations | ||
|
||
from collections.abc import Callable | ||
from dataclasses import dataclass | ||
from datetime import datetime | ||
|
||
from starlink_grpc import StatusDict | ||
|
||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.typing import StateType | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN | ||
from .coordinator import StarlinkUpdateCoordinator | ||
|
||
|
||
@dataclass | ||
class StarlinkSensorEntityDescriptionMixin: | ||
"""Mixin for required keys.""" | ||
|
||
value_fn: Callable[[StatusDict], datetime | StateType] | ||
|
||
|
||
@dataclass | ||
class StarlinkSensorEntityDescription( | ||
SensorEntityDescription, StarlinkSensorEntityDescriptionMixin | ||
): | ||
"""Describes a Starlink sensor entity.""" | ||
|
||
|
||
class StarlinkSensorEntity(CoordinatorEntity[StarlinkUpdateCoordinator], SensorEntity): | ||
"""A SensorEntity that is registered under the Starlink device, and handles creating unique IDs.""" | ||
|
||
entity_description: StarlinkSensorEntityDescription | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__( | ||
self, | ||
coordinator: StarlinkUpdateCoordinator, | ||
description: StarlinkSensorEntityDescription, | ||
) -> None: | ||
"""Initialize the sensor and set the update coordinator.""" | ||
super().__init__(coordinator) | ||
self.entity_description = description | ||
self._attr_unique_id = f"{self.coordinator.data['id']}_{description.key}" | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={ | ||
(DOMAIN, self.coordinator.data["id"]), | ||
}, | ||
sw_version=self.coordinator.data["software_version"], | ||
hw_version=self.coordinator.data["hardware_version"], | ||
name="Starlink", | ||
configuration_url=f"http://{self.coordinator.channel_context.target.split(':')[0]}", | ||
manufacturer="SpaceX", | ||
model="Starlink", | ||
) | ||
|
||
@property | ||
def native_value(self) -> StateType | datetime: | ||
"""Calculate the sensor value from the entity description.""" | ||
return self.entity_description.value_fn(self.coordinator.data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"domain": "starlink", | ||
"name": "Starlink", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/starlink", | ||
"requirements": ["starlink-grpc-core==1.1.1"], | ||
"codeowners": ["@boswelja"], | ||
"iot_class": "local_polling" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"""Contains sensors exposed by the Starlink integration.""" | ||
from __future__ import annotations | ||
|
||
from datetime import datetime, timedelta | ||
|
||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import DEGREE, UnitOfDataRate, UnitOfTime | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity import EntityCategory | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from .const import DOMAIN | ||
from .entity import StarlinkSensorEntity, StarlinkSensorEntityDescription | ||
|
||
SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( | ||
StarlinkSensorEntityDescription( | ||
key="ping", | ||
name="Ping", | ||
icon="mdi:speedometer", | ||
state_class=SensorStateClass.MEASUREMENT, | ||
native_unit_of_measurement=UnitOfTime.MILLISECONDS, | ||
value_fn=lambda data: round(data["pop_ping_latency_ms"]), | ||
), | ||
StarlinkSensorEntityDescription( | ||
key="azimuth", | ||
name="Azimuth", | ||
icon="mdi:compass", | ||
state_class=SensorStateClass.MEASUREMENT, | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
native_unit_of_measurement=DEGREE, | ||
value_fn=lambda data: round(data["direction_azimuth"]), | ||
), | ||
StarlinkSensorEntityDescription( | ||
key="elevation", | ||
name="Elevation", | ||
icon="mdi:compass", | ||
state_class=SensorStateClass.MEASUREMENT, | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
native_unit_of_measurement=DEGREE, | ||
value_fn=lambda data: round(data["direction_elevation"]), | ||
), | ||
StarlinkSensorEntityDescription( | ||
key="uplink_throughput", | ||
name="Uplink throughput", | ||
icon="mdi:upload", | ||
state_class=SensorStateClass.MEASUREMENT, | ||
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, | ||
value_fn=lambda data: round(data["uplink_throughput_bps"]), | ||
), | ||
StarlinkSensorEntityDescription( | ||
key="downlink_throughput", | ||
name="Downlink throughput", | ||
icon="mdi:download", | ||
state_class=SensorStateClass.MEASUREMENT, | ||
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, | ||
value_fn=lambda data: round(data["downlink_throughput_bps"]), | ||
), | ||
StarlinkSensorEntityDescription( | ||
key="last_boot_time", | ||
name="Last boot time", | ||
icon="mdi:clock", | ||
device_class=SensorDeviceClass.TIMESTAMP, | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
value_fn=lambda data: datetime.now().astimezone() | ||
- timedelta(seconds=data["uptime"]), | ||
), | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback | ||
) -> None: | ||
"""Set up all sensors for this entry.""" | ||
coordinator = hass.data[DOMAIN][entry.entry_id] | ||
|
||
async_add_entities( | ||
StarlinkSensorEntity(coordinator, description) for description in SENSORS | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"config": { | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||
}, | ||
"step": { | ||
"user": { | ||
"data": { | ||
"ip_address": "[%key:common::config_flow::data::ip%]" | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"config": { | ||
"abort": { | ||
"already_configured": "Device is already configured" | ||
}, | ||
"error": { | ||
"cannot_connect": "Failed to connect" | ||
}, | ||
"step": { | ||
"user": { | ||
"data": { | ||
"ip_address": "IP Address" | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -403,6 +403,7 @@ | |
"squeezebox", | ||
"srp_energy", | ||
"starline", | ||
"starlink", | ||
"steam_online", | ||
"steamist", | ||
"stookalert", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.