Show Home Assistant sensors on the printer card (#1148, #448)

Binds binary_sensor and reading-carrying sensor entities to a printer and
renders their state on its card, worded by Home Assistant's device_class.
Optional per-sensor alert condition drives a notification on the transition
into the alert state and an opt-in interlock that holds queued prints while
alerting -- a hold with a readable waiting_reason, never a failure, and only
ever on a sensor that was read successfully.

Sensors get their own table rather than a wider entity pattern on SmartPlug:
get_smart_plug_by_printer would otherwise hand the card's power button a door
contact to switch.

The hold is passed to the model matcher directly rather than merged into
busy_printers: _check_auto_drying reads that set as "is currently printing"
and would put an idle-but-held printer down the mid-print drying path.

The notification_providers migration spells its default FALSE, not 0 --
Postgres rejects an integer default for a boolean and _safe_execute swallows
the error.
This commit is contained in:
maziggy
2026-08-05 14:26:38 +02:00
parent 945d4ca6eb
commit cd004df817
45 changed files with 3782 additions and 172 deletions
+88
View File
@@ -2167,6 +2167,75 @@ export interface HATestConnectionResult {
error: string | null;
}
// A Home Assistant entity bound to a printer for display on its card (#1148, #448).
// Read-only: unlike a SmartPlug there is nothing here to switch.
export interface PrinterHASensor {
id: number;
printer_id: number;
name: string;
entity_id: string;
kind: 'binary' | 'numeric';
device_class: string | null; // HA's own class: "door", "temperature", ...
unit: string | null; // numeric sensors only
// What counts as needing attention. Binary sensors use alert_state, numeric
// ones the thresholds; all null means the sensor is display-only.
alert_state: 'on' | 'off' | null;
alert_above: number | null;
alert_below: number | null;
block_print: boolean; // hold the printer's queue while alerting
notify_on_alert: boolean;
show_on_printer_card: boolean;
sort_order: number;
last_state: string | null;
last_changed: string | null;
last_checked: string | null;
created_at: string;
updated_at: string;
}
export interface PrinterHASensorReading {
id: number;
name: string;
entity_id: string;
kind: 'binary' | 'numeric';
device_class: string | null;
unit: string | null;
state: string | null; // null when unreadable or not yet polled
value: number | null; // numeric sensors only
alerting: boolean;
block_print: boolean;
reachable: boolean;
last_changed: string | null;
}
export interface PrinterHASensorCreate {
printer_id: number;
name: string;
entity_id: string;
kind: 'binary' | 'numeric';
device_class?: string | null;
unit?: string | null;
alert_state?: 'on' | 'off' | null;
alert_above?: number | null;
alert_below?: number | null;
block_print?: boolean;
notify_on_alert?: boolean;
show_on_printer_card?: boolean;
sort_order?: number;
}
export type PrinterHASensorUpdate = Partial<Omit<PrinterHASensorCreate, 'printer_id'>>;
// An entity offered by the binding picker.
export interface HADisplayEntity {
entity_id: string;
friendly_name: string;
state: string | null;
domain: string; // "binary_sensor" | "sensor"
device_class: string | null;
unit_of_measurement: string | null;
}
export interface SmartPlugEnergy {
power: number | null; // Current watts
voltage: number | null; // Volts
@@ -2615,6 +2684,7 @@ export interface NotificationProvider {
on_plate_clear_required: boolean;
// Bed cooled
on_bed_cooled: boolean;
on_ha_sensor_alert: boolean;
// First layer complete
on_first_layer_complete: boolean;
// Inventory stock alerts
@@ -2675,6 +2745,7 @@ export interface NotificationProviderCreate {
on_plate_clear_required?: boolean;
// Bed cooled
on_bed_cooled?: boolean;
on_ha_sensor_alert?: boolean;
// First layer complete
on_first_layer_complete?: boolean;
// Inventory stock alerts
@@ -2728,6 +2799,7 @@ export interface NotificationProviderUpdate {
on_plate_clear_required?: boolean;
// Bed cooled
on_bed_cooled?: boolean;
on_ha_sensor_alert?: boolean;
// First layer complete
on_first_layer_complete?: boolean;
// Inventory stock alerts
@@ -5195,6 +5267,22 @@ export const api = {
getHASensorEntities: () =>
request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
// Home Assistant sensors bound to a printer (#1148, #448)
getHASensors: (printerId?: number) =>
request<PrinterHASensor[]>(`/ha-sensors/${printerId ? `?printer_id=${printerId}` : ''}`),
getHASensorReadings: (printerId: number) =>
request<PrinterHASensorReading[]>(`/ha-sensors/by-printer/${printerId}/readings`),
getBindableHAEntities: (search?: string) => {
const params = search ? `?search=${encodeURIComponent(search)}` : '';
return request<HADisplayEntity[]>(`/ha-sensors/entities${params}`);
},
createHASensor: (data: PrinterHASensorCreate) =>
request<PrinterHASensor>('/ha-sensors/', { method: 'POST', body: JSON.stringify(data) }),
updateHASensor: (id: number, data: PrinterHASensorUpdate) =>
request<PrinterHASensor>(`/ha-sensors/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
deleteHASensor: (id: number) =>
request<{ message: string }>(`/ha-sensors/${id}`, { method: 'DELETE' }),
// REST smart plug
testRESTConnection: (url: string, method: string = 'GET', headers?: string | null) =>
request<{ success: boolean; error: string | null }>('/smart-plugs/rest/test-connection', {