Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
38a1829
Create Starlink integration
boswelja Sep 6, 2022
45355ec
Update starlink-grpc-core
boswelja Sep 19, 2022
df2bd9d
Run hassfest
boswelja Sep 28, 2022
6c95fff
Bump GRPCIO to support new integration
boswelja Oct 14, 2022
6cb0519
Revert "Bump GRPCIO to support new integration"
boswelja Oct 15, 2022
0005729
Pin grpcio-reflection
boswelja Oct 16, 2022
6db0093
Create Starlink integration
boswelja Sep 6, 2022
4b821c3
Update starlink-grpc-core
boswelja Sep 19, 2022
ddad778
Bump GRPCIO to support new integration
boswelja Oct 14, 2022
dcb7da4
Revert "Bump GRPCIO to support new integration"
boswelja Oct 15, 2022
b748522
Pin grpcio-reflection
boswelja Oct 16, 2022
3b51ab3
Use a more random "address" for starlink tests
boswelja Oct 20, 2022
81d905e
Fix tests
boswelja Oct 20, 2022
8152e02
Update .strict-typing
boswelja Oct 20, 2022
3bafb5a
Run hassfest
boswelja Oct 25, 2022
452560e
Merge branch 'dev' into feature/starlink-integration
boswelja Dec 24, 2022
4b8d153
Fix deprecated constants
boswelja Dec 25, 2022
3e8b863
Merge branch 'dev' into feature/starlink-integration
boswelja Jan 5, 2023
4f9c47d
Extract some patchers for device found/no device found
boswelja Jan 5, 2023
73a0357
Fix patchers
boswelja Jan 5, 2023
ce70dba
Bump starlink-grpc-core
boswelja Jan 5, 2023
1a4fc58
Apply some changes from code review
boswelja Jan 5, 2023
791f43b
Fix registering multiple Starlink integrations
boswelja Jan 5, 2023
679faca
Create a test to make sure the flow is aborted when trying to create …
boswelja Jan 5, 2023
97c845d
Fix up test_init
boswelja Jan 5, 2023
c542fd9
Update homeassistant/components/starlink/strings.json
boswelja Jan 6, 2023
48d1828
Update homeassistant/components/starlink/config_flow.py
boswelja Jan 6, 2023
f7db542
Regenerate strings
boswelja Jan 6, 2023
3f9bc2b
Update test to make sure the user can still successfully configure th…
boswelja Jan 6, 2023
4a9749f
Update homeassistant/components/starlink/__init__.py
boswelja Jan 6, 2023
1ff7e40
Configure DeviceInfo in entity __init__
boswelja Jan 6, 2023
0252e14
Update homeassistant/components/starlink/coordinator.py
boswelja Jan 6, 2023
7ac4254
Update homeassistant/components/starlink/config_flow.py
boswelja Jan 6, 2023
5f481a7
Fix indent
boswelja Jan 6, 2023
11582a7
Add missing fields to patchers
boswelja Jan 6, 2023
eb34110
Merge branch 'dev' into feature/starlink-integration
bdraco Jan 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,9 @@ omit =
homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py
homeassistant/components/starlink/coordinator.py
homeassistant/components/starlink/entity.py
homeassistant/components/starlink/sensor.py
homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ build.json @home-assistant/supervisor
/tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @fabaff @ThomDietrich
/tests/components/statistics/ @fabaff @ThomDietrich
/homeassistant/components/steam_online/ @tkdrob
Expand Down
35 changes: 35 additions & 0 deletions homeassistant/components/starlink/__init__.py
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
52 changes: 52 additions & 0 deletions homeassistant/components/starlink/config_flow.py
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
3 changes: 3 additions & 0 deletions homeassistant/components/starlink/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the Starlink integration."""

DOMAIN = "starlink"
38 changes: 38 additions & 0 deletions homeassistant/components/starlink/coordinator.py
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
64 changes: 64 additions & 0 deletions homeassistant/components/starlink/entity.py
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):
Comment thread
boswelja marked this conversation as resolved.
Comment thread
boswelja marked this conversation as resolved.
"""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)
9 changes: 9 additions & 0 deletions homeassistant/components/starlink/manifest.json
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"
}
79 changes: 79 additions & 0 deletions homeassistant/components/starlink/sensor.py
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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use our dt utils as far as possible.

def now(time_zone: dt.tzinfo | None = None) -> dt.datetime:
"""Get now in specified time zone."""

- timedelta(seconds=data["uptime"]),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this calculation stable so that the timestamp state doesn't update spammily?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't noticed any issues, but I also haven't run this for longer than maybe a minute at a time. I can leave it polling for a while and see if you suspect there might be an issue?

),
)


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
)
17 changes: 17 additions & 0 deletions homeassistant/components/starlink/strings.json
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%]"
}
}
}
}
}
17 changes: 17 additions & 0 deletions homeassistant/components/starlink/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
Comment thread
boswelja marked this conversation as resolved.
Comment thread
boswelja marked this conversation as resolved.
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect"
},
"step": {
"user": {
"data": {
"ip_address": "IP Address"
}
}
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@
"squeezebox",
"srp_energy",
"starline",
"starlink",
"steam_online",
"steamist",
"stookalert",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -5161,6 +5161,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"starlink": {
"name": "Starlink",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"startca": {
"name": "Start.ca",
"integration_type": "hub",
Expand Down
1 change: 1 addition & 0 deletions homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ httplib2>=0.19.0
# want to ensure we have wheels built.
grpcio==1.51.1
grpcio-status==1.51.1
grpcio-reflection==1.51.1

# libcst >=0.4.0 requires a newer Rust than we currently have available,
# thus our wheels builds fail. This pins it to the last working version,
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,9 @@ starline==0.1.5
# homeassistant.components.starlingbank
starlingbank==3.2

# homeassistant.components.starlink
starlink-grpc-core==1.1.1

# homeassistant.components.statsd
statsd==3.2.1

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,9 @@ srpenergy==1.3.6
# homeassistant.components.starline
starline==0.1.5

# homeassistant.components.starlink
starlink-grpc-core==1.1.1

# homeassistant.components.statsd
statsd==3.2.1

Expand Down
1 change: 1 addition & 0 deletions script/gen_requirements_all.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
# want to ensure we have wheels built.
Comment thread
MartinHjelmare marked this conversation as resolved.
grpcio==1.51.1
grpcio-status==1.51.1
grpcio-reflection==1.51.1

# libcst >=0.4.0 requires a newer Rust than we currently have available,
# thus our wheels builds fail. This pins it to the last working version,
Expand Down
Loading