-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
Tilt Pi integration #139726
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
Tilt Pi integration #139726
Changes from all commits
c4d2c1f
f1b7d61
f2bc36f
f13ecb3
679bd80
a9f80bf
e5c775a
b6ad9c1
844a1d4
84ed1b8
58b8c38
78cde3d
f2c70f5
44c3c2c
1eb10e4
dff4984
9503c5e
beb38e3
bed25e0
cd0a178
d558469
a86ea49
9a39f48
1965042
5c7b37d
3e91a19
c197354
e91eb08
a2b49f6
65d572b
9f1a0c4
02a57ca
7e9499e
806774b
6589045
a3a6054
d78b02d
5c47da2
d8eeae9
1c1d93f
05700b9
735d7f9
252626d
2a487ce
9b738c0
a725e78
834dcfd
3b92fa8
5a3e545
22bdffa
158056c
a0e0d25
0cebd49
8aec831
28da7aa
ad2c399
b7033d5
cb84f15
c181f1c
33c8855
6322d16
30882d2
ddcf2af
de5a6df
2e82b58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"domain": "tilt", | ||
"name": "Tilt", | ||
"integrations": ["tilt_ble", "tilt_pi"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""The Tilt Pi integration.""" | ||
|
||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .coordinator import TiltPiConfigEntry, TiltPiDataUpdateCoordinator | ||
|
||
PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: TiltPiConfigEntry) -> bool: | ||
"""Set up Tilt Pi from a config entry.""" | ||
coordinator = TiltPiDataUpdateCoordinator( | ||
hass, | ||
entry, | ||
) | ||
|
||
await coordinator.async_config_entry_first_refresh() | ||
entry.runtime_data = coordinator | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: TiltPiConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
"""Config flow for Tilt Pi integration.""" | ||
|
||
from typing import Any | ||
|
||
import aiohttp | ||
from tiltpi import TiltPiClient, TiltPiError | ||
import voluptuous as vol | ||
from yarl import URL | ||
|
||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_URL | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class TiltPiConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Tilt Pi.""" | ||
|
||
async def _check_connection(self, host: str, port: int) -> str | None: | ||
"""Check if we can connect to the TiltPi instance.""" | ||
client = TiltPiClient( | ||
host, | ||
port, | ||
session=async_get_clientsession(self.hass), | ||
) | ||
try: | ||
await client.get_hydrometers() | ||
except (TiltPiError, TimeoutError, aiohttp.ClientError): | ||
return "cannot_connect" | ||
return None | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle a configuration flow initialized by the user.""" | ||
|
||
errors = {} | ||
if user_input is not None: | ||
url = URL(user_input[CONF_URL]) | ||
if (host := url.host) is None: | ||
errors[CONF_URL] = "invalid_host" | ||
else: | ||
self._async_abort_entries_match({CONF_HOST: host}) | ||
port = url.port | ||
assert port | ||
error = await self._check_connection(host=host, port=port) | ||
if error: | ||
errors["base"] = error | ||
else: | ||
return self.async_create_entry( | ||
title="Tilt Pi", | ||
data={ | ||
CONF_HOST: host, | ||
CONF_PORT: port, | ||
}, | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=vol.Schema({vol.Required(CONF_URL): str}), | ||
errors=errors, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"""Constants for t 9E81 he Tilt Pi integration.""" | ||
|
||
import logging | ||
from typing import Final | ||
|
||
LOGGER = logging.getLogger(__package__) | ||
|
||
DOMAIN: Final = "tilt_pi" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"""Data update coordinator for Tilt Pi.""" | ||
|
||
from datetime import timedelta | ||
from typing import Final | ||
|
||
from tiltpi import TiltHydrometerData, TiltPiClient, TiltPiError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_HOST, CONF_PORT | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import LOGGER | ||
|
||
SCAN_INTERVAL: Final = timedelta(seconds=60) | ||
|
||
type TiltPiConfigEntry = ConfigEntry[TiltPiDataUpdateCoordinator] | ||
|
||
|
||
class TiltPiDataUpdateCoordinator(DataUpdateCoordinator[dict[str, TiltHydrometerData]]): | ||
"""Class to manage fetching Tilt Pi data.""" | ||
|
||
config_entry: TiltPiConfigEntry | ||
|
||
def __init__( | ||
self, | ||
hass: HomeAssistant, | ||
config_entry: TiltPiConfigEntry, | ||
) -> None: | ||
"""Initialize the coordinator.""" | ||
super().__init__( | ||
hass, | ||
LOGGER, | ||
config_entry=config_entry, | ||
name="Tilt Pi", | ||
update_interval=SCAN_INTERVAL, | ||
) | ||
self._api = TiltPiClient( | ||
host=config_entry.data[CONF_HOST], | ||
port=config_entry.data[CONF_PORT], | ||
session=async_get_clientsession(hass), | ||
) | ||
self.identifier = config_entry.entry_id | ||
|
||
async def _async_update_data F438 (self) -> dict[str, TiltHydrometerData]: | ||
"""Fetch data from Tilt Pi and return as a dict keyed by mac_id.""" | ||
try: | ||
hydrometers = await self._api.get_hydrometers() | ||
except TiltPiError as err: | ||
raise UpdateFailed(f"Error communicating with Tilt Pi: {err}") from err | ||
|
||
return {h.mac_id: h for h in hydrometers} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Base entity for Tilt Pi integration.""" | ||
|
||
from tiltpi import TiltHydrometerData | ||
|
||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .coordinator import TiltPiDataUpdateCoordinator | ||
|
||
|
||
class TiltEntity(CoordinatorEntity[TiltPiDataUpdateCoordinator]): | ||
"""Base class for Tilt entities.""" | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__( | ||
self, | ||
coordinator: TiltPiDataUpdateCoordinator, | ||
hydrometer: TiltHydrometerData, | ||
) -> None: | ||
"""Initialize the entity.""" | ||
super().__init__(coordinator) | ||
self._mac_id = hydrometer.mac_id | ||
self._attr_device_info = DeviceInfo( | ||
connections={(CONNECTION_NETWORK_MAC, hydrometer.mac_id)}, | ||
name=f"Tilt {hydrometer.color}", | ||
manufacturer="Tilt Hydrometer", | ||
model=f"{hydrometer.color} Tilt Hydrometer", | ||
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. what do you mean exactly with color? Is it like a model id? 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. My understanding is that the Tilt Pi (the OS that is able to connect via BT to the Tilt Hydrometer devices) can't recognize two Tilts of the same color. The color is then in my view what the different models of the Tilts represent, although that can be discussed. |
||
) | ||
|
||
@property | ||
def current_hydrometer(self) -> TiltHydrometerData: | ||
"""Return the current hydrometer data for this entity.""" | ||
return self.coordinator.data[self._mac_id] | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return True if the hydrometer is available (present in coordinator data).""" | ||
return super().available and self._mac_id in self.coordinator.data |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"entity": { | ||
"sensor": { | ||
"gravity": { | ||
"default": "mdi:water" | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "tilt_pi", | ||
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. since we already have the BLE one, we should create a brand for this 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. Unless I'm misunderstanding, there is already a request open that fulfills this: home-assistant/brands#6667 If that does not match what you are envisioning here please let me know, 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. I mean that we have a folder called brands somewhere where we group devices from the same brand together 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. Apologies if I'm not understanding, but what would that concretely look like? Right now the PR I have open in the brands repo adds the icons under The Would the result of creating a unified brand for this be icons existing in 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. Okay so first the brands concept in core, there's a folder with json files that is called brands and we can create one for tilt pi. This way if you search for tilt pi you automatically get to pick between the two integrations. That brand also needs assets but your assets are likely similar to the BLE one. So you move all your assets to the core_brands and then symlink those to core_integrations tilt_pi and ble 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. I have updated home-assistant/brands#6667 to leverage a new 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. But then this PR still needs a new json file for the brand. If you do a find in the codebase for google.json you can find an example and I think once you see that you'll get what I try to explain :) |
||
"name": "Tilt Pi", | ||
"codeowners": ["@michaelheyman"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/tilt_pi", | ||
"iot_class": "local_polling", | ||
"quality_scale": "bronze", | ||
"requirements": ["tilt-pi==0.2.1"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
rules: | ||
# Bronze | ||
action-setup: done | ||
appropriate-polling: done | ||
brands: done | ||
common-modules: done | ||
config-flow-test-coverage: done | ||
config-flow: done | ||
dependency-transparency: done | ||
docs-actions: | ||
status: exempt | ||
comment: | | ||
This integration does not provide additional actions. | ||
docs-high-level-description: done | ||
docs-installation-instructions: done | ||
docs-removal-instructions: done | ||
entity-event-setup: | ||
status: exempt | ||
comment: | | ||
Entities of this integration does not explicitly subscribe to events. | ||
entity-unique-id: done | ||
has-entity-name: done | ||
runtime-data: done | ||
test-before-configure: done | ||
test-before-setup: done | ||
unique-config-entry: done | ||
|
||
# Silver | ||
action-exceptions: | ||
status: exempt | ||
comment: | | ||
No custom actions are defined. | ||
Comment on lines
+29
to
+32
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. nothing to do here atm, but keep in mind that'll also apply to any actions that are part of HA (like switching a switch) so you'll need to revisit that in case you add other platforms |
||
config-entry-unloading: done | ||
docs-configuration-parameters: | ||
status: exempt | ||
comment: No options to configure | ||
docs-installation-parameters: done | ||
entity-unavailable: done | ||
integration-owner: done | ||
log-when-unavailable: done | ||
parallel-updates: done | ||
reauthentication-flow: | ||
status: exempt | ||
comment: | | ||
This integration does not require authentication. | ||
test-coverage: done | ||
# Gold | ||
devices: done | ||
diagnostics: todo | ||
discovery-update-info: todo | ||
discovery: todo | ||
docs-data-update: todo | ||
docs-examples: todo | ||
docs-known-limitations: done | ||
docs-supported-devices: done | ||
docs-supported-functions: done | ||
docs-troubleshooting: todo | ||
docs-use-cases: todo | ||
dynamic-devices: todo | ||
entity-category: | ||
status: done | ||
comment: | | ||
The entities are categorized well by using default category. | ||
entity-device-class: done | ||
entity-disabled-by-default: | ||
status: exempt | ||
comment: No disabled entities implemented | ||
entity-translations: done | ||
exception-translations: todo | ||
icon-translations: done | ||
reconfiguration-flow: todo | ||
repair-issues: | ||
status: exempt | ||
comment: | | ||
No repairs/issues. | ||
stale-devices: todo | ||
# Platinum | ||
async-dependency: done | ||
inject-websession: done | ||
strict-typing: todo |
Uh oh!
There was an error while loading. Please reload this page.