8000 Tilt Pi integration by michaelheyman · Pull Request #139726 · home-assistant/core · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
merged 65 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c4d2c1f
Create component via script.scaffold
michaelheyman Feb 23, 2025
f1b7d61
Create sensor definition
michaelheyman Feb 23, 2025
f2bc36f
Define coordinator
michaelheyman Feb 23, 2025
f13ecb3
Define config flow
michaelheyman Feb 24, 2025
679bd80
Refine sensor definition and add tests
michaelheyman Feb 25, 2025
a9f80bf
Refine coordinator after testing end to end
michaelheyman Feb 27, 2025
e5c775a
Redefine sensor in a more idiomatic way
michaelheyman Feb 27, 2025
b6ad9c1
Use entity (common-module)
michaelheyman Feb 27, 2025
844a1d4
Follow config-flow conventions more closely
michaelheyman Feb 27, 2025
84ed1b8
Use custom ConfigEntry to conform to strict-typing
michaelheyman Feb 27, 2025
58b8c38
Define API object instead of using aio directly
michaelheyman Feb 27, 2025
78cde3d
Test before setup in init
michaelheyman Feb 28, 2025
f2c70f5
Add diagnostics
michaelheyman Feb 28, 2025
44c3c2c
Make some more quality changes
michaelheyman Mar 1, 2025
1eb10e4
Move scan interval to const
michaelheyman Mar 3, 2025
dff4984
Commit generated files
michaelheyman Mar 3, 2025
9503c5e
Add quality scale
michaelheyman Mar 3, 2025
beb38e3
Merge branch 'dev' into tilt-pi
michaelheyman Mar 4, 2025
bed25e0
feedback: Apply consistent language to Tilt Pi refs
michaelheyman Mar 5, 2025
cd0a178
Merge branch 'dev' into tilt-pi
michaelheyman Mar 6, 2025
d558469
feedback: Remove empty manifest fields
michaelheyman May 31, 2025
a86ea49
feedback: Use translations instead of hardcoded name
michaelheyman May 31, 2025
9a39f48
feedback: Remove diagnostics
michaelheyman Jun 1, 2025
1965042
feedback: Idiomatic and general improvements
michaelheyman Jun 1, 2025
5c7b37d
Merge branch 'dev' into tilt-pi
michaelheyman Jun 2, 2025
3e91a19
Use tilt-pi library
michaelheyman Jun 8, 2025
c197354
feedback: Coordinator data returns dict
michaelheyman Jun 9, 2025
e91eb08
feedback: Move client creation to coordinator
michaelheyman Jun 9, 2025
a2b49f6
feedback: Request only Tilt Pi URL from user
michaelheyman Jun 9, 2025
65d572b
Merge branch 'dev' into tilt-pi
michaelheyman Jun 10, 2025
9f1a0c4
Update homeassistant/components/tilt_pi/entity.py
michaelheyman Jun 11, 2025
02a57ca
Update homeassistant/components/tilt_pi/sensor.py
michaelheyman Jun 11, 2025
7e9499e
Update homeassistant/components/tilt_pi/entity.py
michaelheyman Jun 11, 2025
806774b
feedback: Avoid redundant keyword arguments in function calls
michaelheyman Jun 11, 2025
6589045
feedback: Remove unused models and variables
michaelheyman Jun 12, 2025
a3a6054
feedback: Use icons.json
michaelheyman Jun 12, 2025
d78b02d
feedback: Style best practices
michaelheyman Jun 13, 2025
5c47da2
Merge branch 'dev' into tilt-pi
michaelheyman Jun 13, 2025
d8eeae9
Update homeassistant/components/tilt_pi/entity.py
michaelheyman Jun 14, 2025
1c1d93f
Update tests/components/tilt_pi/test_config_flow.py
michaelheyman Jun 14, 2025
05700b9
feedback: Improve config flow unit tests
michaelheyman Jun 14, 2025
735d7f9
feedback: Patch TiltPi client mock
michaelheyman Jun 14, 2025
252626d
feedback: Mark entity-device-class as done
michaelheyman Jun 14, 2025
2a487ce
feedback: Align quaity scale with current state
michaelheyman Jun 15, 2025
9b738c0
feeback: Create brands file for Tilt brand
michaelheyman Jun 15, 2025
a725e78
feedback: Demonstrate recovery in config flow
michaelheyman Jun 15, 2025
834dcfd
feedback: Test coordinator behavior via sensors
michaelheyman Jun 15, 2025
3b92fa8
Merge branch 'dev' into tilt-pi
michaelheyman Jun 15, 2025
5a3e545
Update homeassistant/components/tilt_pi/config_flow.py
michaelheyman Jun 17, 2025
22bdffa
Update homeassistant/components/tilt_pi/coordinator.py
michaelheyman Jun 17, 2025
158056c
Update homeassistant/components/tilt_pi/quality_scale.yaml
michaelheyman Jun 17, 2025
a0e0d25
Update homeassistant/components/tilt_pi/quality_scale.yaml
michaelheyman Jun 17, 2025
0cebd49
Update homeassistant/components/tilt_pi/quality_scale.yaml
michaelheyman Jun 17, 2025
8aec831
Update homeassistant/components/tilt_pi/config_flow.py
michaelheyman Jun 17, 2025
28da7aa
feedback: Update tilt_pi quality scale
michaelheyman Jun 17, 2025
ad2c399
feedback: Move const to coordinator
michaelheyman Jun 17, 2025
b7033d5
feedback: Correct strings.json for incorrect and missing fields
michaelheyman Jun 17, 2025
cb84f15
feedback: Use tiltpi package version published via CI
michaelheyman Jun 17, 2025
c181f1c
Merge branch 'dev' into tilt-pi
michaelheyman Jun 17, 2025
33c8855
Run ruff format manually
michaelheyman Jun 17, 2025
6322d16
Merge branch 'dev' into tilt-pi
michaelheyman Jun 17, 2025
30882d2
Add missing string for invalid host
michaelheyman Jun 17, 2025
ddcf2af
Merge branch 'dev' into tilt-pi
joostlek Jun 23, 2025
de5a6df
Fix
joostlek Jun 23, 2025
2e82b58
Fix
joostlek Jun 23, 2025
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
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions homeassistant/brands/tilt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"domain": "tilt",
"name": "Tilt",
"integrations": ["tilt_ble", "tilt_pi"]
}
28 changes: 28 additions & 0 deletions homeassistant/components/tilt_pi/__init__.py
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)
63 changes: 63 additions & 0 deletions homeassistant/components/tilt_pi/config_flow.py
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,
)
8 changes: 8 additions & 0 deletions homeassistant/components/tilt_pi/const.py
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"
53 changes: 53 additions & 0 deletions homeassistant/components/tilt_pi/coordinator.py
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}
39 changes: 39 additions & 0 deletions homeassistant/components/tilt_pi/entity.py
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",
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
9 changes: 9 additions & 0 deletions homeassistant/components/tilt_pi/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"gravity": {
"default": "mdi:water"
}
}
}
}
10 changes: 10 additions & 0 deletions homeassistant/components/tilt_pi/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "tilt_pi",
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 core_integrations/tilt_pi.

The tilt_ble integration icons live in core_integrations/tilt_ble.

Would the result of creating a unified brand for this be icons existing in core_brands/tilt? And what does that mean for the change in this manifest file? Does it require a change in the tilt_ble component, and if so should that particular change be in a separate PR?

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have updated home-assistant/brands#6667 to leverage a new core_brands/tilt symlinked to the tilt_pi and tilt_ble core integrations.

Copy link
Member

Choose a reason for hiding this comment

The 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"]
}
80 changes: 80 additions & 0 deletions homeassistant/components/tilt_pi/quality_scale.yaml
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
Copy link
Member

Choose a reason for hiding this comment

The 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
Loading
0