55import logging
66from typing import Any
77
8- from homeassistant .components .sensor import SensorEntity
8+ from homeassistant .components .sensor import (
9+ SensorDeviceClass ,
10+ SensorEntity ,
11+ SensorStateClass ,
12+ )
913from homeassistant .core import callback
1014from homeassistant .helpers .device_registry import DeviceInfo
1115from homeassistant .helpers .entity import generate_entity_id
1519
1620_LOGGER = logging .getLogger (__name__ )
1721
22+ _NUMERIC_FIELD_TYPES = {"integer" , "float" }
23+
24+ # Map normalized Spoolman unit strings to (device_class, icon).
25+ # Matched on the unit reported by Spoolman, not on the user-chosen field name,
26+ # so it works regardless of locale.
27+ _UNIT_DEVICE_CLASS = {
28+ "%" : (SensorDeviceClass .HUMIDITY , "mdi:water-percent" ),
29+ "°c" : (SensorDeviceClass .TEMPERATURE , "mdi:thermometer" ),
30+ "°f" : (SensorDeviceClass .TEMPERATURE , "mdi:thermometer" ),
31+ "k" : (SensorDeviceClass .TEMPERATURE , "mdi:thermometer" ),
32+ "g" : (SensorDeviceClass .WEIGHT , "mdi:weight-gram" ),
33+ "kg" : (SensorDeviceClass .WEIGHT , "mdi:weight-kilogram" ),
34+ "mm" : (SensorDeviceClass .DISTANCE , "mdi:ruler" ),
35+ "m" : (SensorDeviceClass .DISTANCE , "mdi:ruler" ),
36+ }
37+
38+
39+ def _device_class_for (field_type : str | None , unit : str | None ) -> tuple [Any , str ]:
40+ """Pick a device class + icon based on Spoolman field metadata."""
41+ if field_type == "datetime" :
42+ return SensorDeviceClass .TIMESTAMP , "mdi:calendar"
43+ if unit :
44+ match = _UNIT_DEVICE_CLASS .get (unit .strip ().lower ())
45+ if match :
46+ return match
47+ return None , "mdi:tag-outline"
48+
1849
1950class SpoolExtraField (CoordinatorEntity , SensorEntity ):
2051 """Sensor for spool extra field - dynamically created for each extra field."""
@@ -42,6 +73,19 @@ def __init__(
4273 else :
4374 spool_name = f"Spoolman Spool { self ._spool ['id' ]} "
4475
76+ # Pull metadata reported by Spoolman (`/api/v1/field/spool`) so we can
77+ # set proper device class / unit / state class. Falls back to a derived
78+ # display name and a plain icon when metadata isn't available.
79+ field_meta = (
80+ (coordinator .data or {})
81+ .get ("extra_fields" , {})
82+ .get ("spool" , {})
83+ .get (field_key , {})
84+ )
85+ display_name = field_meta .get ("name" ) or field_key .replace ("_" , " " ).title ()
86+ field_type = field_meta .get ("field_type" )
87+ unit = field_meta .get ("unit" )
88+
4589 # Create entity ID with the field key
4690 # e.g., sensor.spoolman_spool_1_extra_humidity
4791 safe_field_key = field_key .lower ().replace (" " , "_" ).replace ("-" , "_" )
@@ -52,8 +96,19 @@ def __init__(
5296 )
5397 self ._attr_unique_id = f"spoolman_{ self ._entry .entry_id } _spool_{ spool_data ['id' ]} _extra_{ safe_field_key } "
5498 self ._attr_has_entity_name = False
55- self ._attr_name = f"{ spool_name } Extra { field_key .replace ('_' , ' ' ).title ()} "
56- self ._attr_icon = "mdi:tag-outline"
99+ self ._attr_name = f"{ spool_name } Extra { display_name } "
100+
101+ device_class , icon = _device_class_for (field_type , unit )
102+ if device_class is not None :
103+ self ._attr_device_class = device_class
104+ self ._attr_icon = icon
105+
106+ if field_type in _NUMERIC_FIELD_TYPES :
107+ self ._attr_state_class = SensorStateClass .MEASUREMENT
108+
109+ if unit :
110+ self ._attr_native_unit_of_measurement = unit
111+
57112 self ._attr_device_info = DeviceInfo (
58113 identifiers = {(DOMAIN , self .config [CONF_URL ], f"spool_{ self ._spool ['id' ]} " )} # type: ignore[arg-type]
59114 )
@@ -91,6 +146,7 @@ def state(self) -> Any:
91146 # Convert value to string if it's a complex type
92147 if isinstance (value , dict | list ):
93148 import json
149+
94150 return json .dumps (value )
95151
96152 return value
0 commit comments