27
27
28
28
from homeassistant .components .file_upload import process_uploaded_file
29
29
from homeassistant .components .hassio import AddonError , AddonManager , AddonState
30
+ from homeassistant .components .sensor import (
31
+ CONF_STATE_CLASS ,
32
+ DEVICE_CLASS_UNITS ,
33
+ SensorDeviceClass ,
34
+ SensorStateClass ,
35
+ )
30
36
from homeassistant .config_entries import (
31
37
SOURCE_RECONFIGURE ,
32
38
ConfigEntry ,
45
51
ATTR_SW_VERSION ,
46
52
CONF_CLIENT_ID ,
47
53
CONF_DEVICE ,
54
+ CONF_DEVICE_CLASS ,
48
55
CONF_DISCOVERY ,
49
56
CONF_HOST ,
50
57
CONF_NAME ,
53
60
CONF_PLATFORM ,
54
61
CONF_PORT ,
55
62
CONF_PROTOCOL ,
63
+ CONF_UNIT_OF_MEASUREMENT ,
56
64
CONF_USERNAME ,
65
+ CONF_VALUE_TEMPLATE ,
57
66
)
58
67
from homeassistant .core import HomeAssistant , callback
59
68
from homeassistant .data_entry_flow import AbortFlow
99
108
CONF_COMMAND_TOPIC ,
100
109
CONF_DISCOVERY_PREFIX ,
101
110
CONF_ENTITY_PICTURE ,
111
+ CONF_EXPIRE_AFTER ,
102
112
CONF_KEEPALIVE ,
113
+ CONF_LAST_RESET_VALUE_TEMPLATE ,
114
+ CONF_OPTIONS ,
103
115
CONF_PAYLOAD_AVAILABLE ,
104
116
CONF_PAYLOAD_NOT_AVAILABLE ,
105
117
CONF_QOS ,
106
118
CONF_RETAIN ,
119
+ CONF_STATE_TOPIC ,
120
+ CONF_SUGGESTED_DISPLAY_PRECISION ,
107
121
CONF_TLS_INSECURE ,
108
122
CONF_TRANSPORT ,
109
123
CONF_WILL_MESSAGE ,
133
147
from .util import (
134
148
async_create_certificate_temp_files ,
135
149
get_file_path ,
150
+ learn_more_url ,
136
151
valid_birth_will ,
137
152
valid_publish_topic ,
138
153
valid_qos_schema ,
139
154
valid_subscribe_topic ,
140
155
valid_subscribe_topic_template ,
156
+ validate_sensor_state_and_device_class_config ,
141
157
)
142
158
143
159
_LOGGER = logging .getLogger (__name__ )
217
233
)
218
234
219
235
# Subentry selectors
220
- SUBENTRY_PLATFORMS = [Platform .NOTIFY ]
236
+ RESET_IF_EMPTY = {CONF_OPTIONS }
237
+ SUBENTRY_PLATFORMS = [Platform .NOTIFY , Platform .SENSOR ]
221
238
SUBENTRY_PLATFORM_SELECTOR = SelectSelector (
222
239
SelectSelectorConfig (
223
240
options = [platform .value for platform in SUBENTRY_PLATFORMS ],
241
258
}
242
259
)
243
260
261
+ # Sensor specific selectors
262
+ SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector (
263
+ SelectSelectorConfig (
264
+ options = [device_class .value for device_class in SensorDeviceClass ],
265
+ mode = SelectSelectorMode .DROPDOWN ,
266
+ translation_key = CONF_DEVICE_CLASS ,
267
+ )
268
+ )
269
+ SENSOR_STATE_CLASS_SELECTOR = SelectSelector (
270
+ SelectSelectorConfig (
271
+ options = [device_class .value for device_class in SensorStateClass ],
272
+ mode = SelectSelectorMode .DROPDOWN ,
273
+ translation_key = CONF_STATE_CLASS ,
274
+ )
275
+ )
276
+ OPTIONS_SELECTOR = SelectSelector (
277
+ SelectSelectorConfig (
278
+ options = [],
279
+ custom_value = True ,
280
+ multiple = True ,
281
+ )
282
+ )
283
+ SUGGESTED_DISPLAY_PRECISION_SELECTOR = NumberSelector (
284
+ NumberSelectorConfig (mode = NumberSelectorMode .BOX , min = 0 , max = 9 )
285
+ )
286
+ EXIRE_AFTER_SELECTOR = NumberSelector (
287
+ NumberSelectorConfig (mode = NumberSelectorMode .BOX , min = 0 )
288
+ )
289
+
244
290
245
291
@dataclass (frozen = True )
246
292
class PlatformField :
247
293
"""Stores a platform config field schema, required flag and validator."""
248
294
249
- selector : Selector
295
+ selector : Selector [ Any ] | Callable [..., Selector [ Any ]]
250
296
required : bool
251
297
validator : Callable [..., Any ]
252
298
error : str | None = None
253
299
default : str | int | vol .Undefined = vol .UNDEFINED
254
300
exclude_from_reconfig : bool = False
301
+ custom_filtering : bool = False
302
+
303
+
304
+ @callback
305
+ def unit_of_measurement_selector (user_data : dict [str , Any | None ]) -> Selector :
306
+ """Return a context based unit of measurement selector."""
307
+ if (
308
+ user_data is None
309
+ or (device_class := user_data .get (CONF_DEVICE_CLASS )) is None
310
+ or device_class not in DEVICE_CLASS_UNITS
311
+ ):
312
+ return TEXT_SELECTOR
313
+ return SelectSelector (
314
+ SelectSelectorConfig (
315
+ options = [str (uom ) for uom in DEVICE_CLASS_UNITS [device_class ]],
316
+ custom_value = True ,
317
+ )
318
+ )
255
319
256
320
257
321
COMMON_ENTITY_FIELDS = {
@@ -264,7 +328,20 @@ class PlatformField:
264
328
265
329
COMMON_MQTT_FIELDS = {
266
330
CONF_QOS : PlatformField (QOS_SELECTOR , False , valid_qos_schema , default = 0 ),
267
- CONF_RETAIN : PlatformField (BOOLEAN_SELECTOR , False , bool ),
331
+ }
332
+ PLATFORM_ENTITY_FIELDS = {
333
+ Platform .NOTIFY .value : {},
334
+ Platform .SENSOR .value : {
335
+ CONF_DEVICE_CLASS : PlatformField (SENSOR_DEVICE_CLASS_SELECTOR , False , str ),
336
+ CONF_STATE_CLASS : PlatformField (SENSOR_STATE_CLASS_SELECTOR , False , str ),
337
+ CONF_UNIT_OF_MEASUREMENT : PlatformField (
338
+ unit_of_measurement_selector , False , str , custom_filtering = True
339
+ ),
340
+ CONF_SUGGESTED_DISPLAY_PRECISION : PlatformField (
341
+ SUGGESTED_DISPLAY_PRECISION_SELECTOR , False , cv .positive_int
342
+ ),
343
+ CONF_OPTIONS : PlatformField (OPTIONS_SELECTOR , False , cv .ensure_list ),
344
+ },
268
345
}
269
346
PLATFORM_MQTT_FIELDS = {
270
347
Platform .NOTIFY .value : {
@@ -274,8 +351,27 @@ class PlatformField:
274
351
CONF_COMMAND_TEMPLATE : PlatformField (
275
352
TEMPLATE_SELECTOR , False , cv .template , "invalid_template"
276
353
),
354
+ CONF_RETAIN : PlatformField (BOOLEAN_SELECTOR , False , bool ),
355
+ },
356
+ Platform .SENSOR .value : {
357
+ CONF_STATE_TOPIC : PlatformField (
358
+ TEXT_SELECTOR , True , valid_subscribe_topic , "invalid_subscribe_topic"
359
+ ),
360
+ CONF_VALUE_TEMPLATE : PlatformField (
361
+ TEMPLATE_SELECTOR , False , cv .template , "invalid_template"
362
+ ),
363
+ CONF_LAST_RESET_VALUE_TEMPLATE : PlatformField (
364
+ TEMPLATE_SELECTOR , False , cv .template , "invalid_template"
365
+ ),
366
+ CONF_EXPIRE_AFTER : PlatformField (EXIRE_AFTER_SELECTOR , False , cv .positive_int ),
277
367
},
278
368
}
369
+ ENTITY_CONFIG_VALIDATOR : dict [
370
+ str , Callable [[dict [str , Any ], dict [str , str ]], dict [str , Any ]] | None
371
+ ] = {
372
+ Platform .NOTIFY .value : None ,
373
+ Platform .SENSOR .value : validate_sensor_state_and_device_class_config ,
374
+ }
279
375
280
376
MQTT_DEVICE_SCHEMA = vol .Schema (
281
377
{
@@ -342,6 +438,9 @@ def validate_user_input(
342
438
user_input : dict [str , Any ],
343
439
data_schema_fields : dict [str , PlatformField ],
344
440
errors : dict [str , str ],
441
+ config_validator : Callable [[dict [str , Any ], dict [str , str ]], dict [str , str ]]
442
+ | None = None ,
443
+ component_data : dict [str , Any ] | None = None ,
345
444
) -> None :
346
445
"""Validate user input."""
347
446
for field , value in user_input .items ():
@@ -351,20 +450,33 @@ def validate_user_input(
351
450
except (ValueError , vol .Invalid ):
352
451
errors [field ] = data_schema_fields [field ].error or "invalid_input"
353
452
453
+ if config_validator is not None :
454
+ config = user_input
455
+ if component_data is not None :
456
+ config |= component_data
457
+ config_validator (config , errors )
458
+
354
459
355
460
@callback
356
461
def data_schema_from_fields (
357
462
data_schema_fields : dict [str , PlatformField ],
358
463
reconfig : bool ,
464
+ component : dict [str , Any ] | None = None ,
465
+ user_input : dict [str , Any ] | None = None ,
359
466
) -> vol .Schema :
360
- """Generate data schema from platform fields."""
467
+ """Generate custom data schema from platform fields."""
468
+ user_data = component
469
+ if user_data is not None and user_input is not None :
470
+ user_data |= user_input
361
471
return vol .Schema (
362
472
{
363
473
vol .Required (field_name , default = field_details .default )
364
474
if field_details .required
365
475
else vol .Optional (
366
476
field_name , default = field_details .default
367
- ): field_details .selector
477
+ ): field_details .selector (user_data ) # type: ignore[operator]
478
+ if field_details .custom_filtering
479
+ else field_details .selector
368
480
for field_name , field_details in data_schema_fields .items ()
369
481
if not field_details .exclude_from_reconfig or not reconfig
370
482
}
@@ -908,6 +1020,34 @@ def update_component_fields(
908
1020
component_data .pop (field )
909
1021
component_data .update (user_input )
910
1022
1023
+ @callback
1024
+ def reset_if_empty (self , user_input : dict [str , Any ]) -> None :
1025
+ """Reset fields in componment config that are not in the user_input."""
1026
+ if TYPE_CHECKING :
1027
+ assert self ._component_id is not None
1028
+ for field in [
1029
+ form_field
1030
+ for form_field in user_input
1031
+ if form_field in RESET_IF_EMPTY
1032
+ and form_field in RESET_IF_EMPTY
1033
+ and not user_input [form_field ]
1034
+ ]:
1035
+ user_input .pop (field )
1036
+
1037
+ @callback
1038
+ def generate_names (self ) -> tuple [str , str ]:
1039
+ """Generate the device and full entity name."""
1040
+ if TYPE_CHECKING :
1041
+ assert self ._component_id is not None
1042
+ device_name = self ._subentry_data [CONF_DEVICE ][CONF_NAME ]
1043
+ if entity_name := self ._subentry_data ["components" ][self ._component_id ].get (
1044
+ CONF_NAME
1045
+ ):
1046
+ full_entity_name : str = f"{ device_name } { entity_name } "
1047
+ else :
1048
+ full_entity_name = device_name
1049
+ return device_name , full_entity_name
1050
+
911
1051
async def async_step_user (
912
1052
self , user_input : dict [str , Any ] | None = None
913
1053
) -> SubentryFlowResult :
@@ -970,7 +1110,7 @@ async def async_step_entity(
970
1110
self ._component_id = uuid4 ().hex
971
1111
self ._subentry_data ["components" ].setdefault (self ._component_id , {})
972
1112
self .update_component_fields (data_schema , user_input )
973
- return await self .async_step_mqtt_platform_config ()
1113
+ return await self .async_step_entity_platform_config ()
974
1114
data_schema = self .add_suggested_values_to_schema (data_schema , user_input )
975
1115
elif self .source == SOURCE_RECONFIGURE and self ._component_id is not None :
976
1116
data_schema = self .add_suggested_values_to_schema (
@@ -1034,6 +1174,57 @@ async def async_step_delete_entity(
1034
1174
return await self .async_step_summary_menu ()
1035
1175
return self ._show_update_or_delete_form ("delete_entity" )
1036
1176
1177
+ async def async_step_entity_platform_config (
1178
+ self , user_input : dict [str , Any ] | None = None
1179
+ ) -> SubentryFlowResult :
1180
+ """Configure platform entity details."""
1181
+ if TYPE_CHECKING :
1182
+ assert self ._component_id is not None
1183
+ component = self ._subentry_data ["components" ][self ._component_id ]
1184
+ platform = component [CONF_PLATFORM ]
1185
+ if not (data_schema_fields := PLATFORM_ENTITY_FIELDS [platform ]):
1186
+ return await self .async_step_mqtt_platform_config ()
1187
+ errors : dict [str , str ] = {}
1188
+
1189
+ data_schema = data_schema_from_fields (
1190
+ data_schema_fields ,
1191
+ reconfig = True ,
1192
+ component = component ,
1193
+ user_input = user_input ,
1194
+ )
1195
+ if user_input is not None :
1196
+ # Test entity fields against the validator
1197
+ self .reset_if_empty (user_input )
1198
+ validate_user_input (
1199
+ user_input ,
1200
+ data_schema_fields ,
1201
+ errors ,
1202
+ ENTITY_CONFIG_VALIDATOR [platform ],
1203
+ )
1204
+ if not errors :
1205
+ self .update_component_fields (data_schema , user_input )
1206
+ return await self .async_step_mqtt_platform_config ()
1207
+
1208
+ data_schema = self .add_suggested_values_to_schema (data_schema , user_input )
1209
+ else :
1210
+ data_schema = self .add_suggested_values_to_schema (
1211
+ data_schema , self ._subentry_data ["components" ][self ._component_id ]
1212
+ )
1213
+
1214
+ device_name , full_entity_name = self .generate_names ()
1215
+ return self .async_show_form (
1216
+ step_id = "entity_platform_config" ,
1217
+ data_schema = data_schema ,
1218
+ description_placeholders = {
1219
+ "mqtt_device" : device_name ,
1220
+ CONF_PLATFORM : platform ,
1221
+ "entity" : full_entity_name ,
1222
+ "url" : learn_more_url (platform ),
1223
+ },
1224
+ errors = errors ,
1225
+ last_step = False ,
1226
+ )
1227
+
1037
1228
async def async_step_mqtt_platform_config (
1038
1229
self , user_input : dict [str , Any ] | None = None
1039
1230
) -> SubentryFlowResult :
@@ -1048,7 +1239,14 @@ async def async_step_mqtt_platform_config(
1048
1239
)
1049
1240
if user_input is not None :
1050
1241
# Test entity fields against the validator
1051
- validate_user_input (user_input , data_schema_fields , errors )
1242
+ self .reset_if_empty (user_input )
1243
+ validate_user_input (
1244
+ user_input ,
1245
+ data_schema_fields ,
1246
+ errors ,
1247
+ ENTITY_CONFIG_VALIDATOR [platform ],
1248
+ self ._subentry_data ["components" ][self ._component_id ],
1249
+ )
1052
1250
if not errors :
1053
1251
self .update_component_fields (data_schema , user_input )
1054
1252
self ._component_id = None
@@ -1061,21 +1259,15 @@ async def async_step_mqtt_platform_config(
1061
1259
data_schema = self .add_suggested_values_to_schema (
1062
1260
data_schema , self ._subentry_data ["components" ][self ._component_id ]
1063
1261
)
1064
- device_name = self ._subentry_data [CONF_DEVICE ][CONF_NAME ]
1065
- entity_name : str | None
1066
- if entity_name := self ._subentry_data ["components" ][self ._component_id ].get (
1067
- CONF_NAME
1068
- ):
1069
- full_entity_name : str = f"{ device_name } { entity_name } "
1070
- else :
1071
- full_entity_name = device_name
1262
+ device_name , full_entity_name = self .generate_names ()
1072
1263
return self .async_show_form (
1073
1264
step_id = "mqtt_platform_config" ,
1074
1265
data_schema = data_schema ,
1075
1266
description_placeholders = {
1076
1267
"mqtt_device" : device_name ,
1077
1268
CONF_PLATFORM : platform ,
1078
1269
"entity" : full_entity_name ,
1270
+ "url" : learn_more_url (platform ),
1079
1271
},
1080
1272
errors = errors ,
1081
1273
last_step = False ,
0 commit comments