1212from math import isinf , isnan
1313from typing import TYPE_CHECKING , Any
1414
15+ from homeassistant .components .lock .const import LockState
1516from homeassistant .components .sun .const import STATE_ABOVE_HORIZON , STATE_BELOW_HORIZON
1617from homeassistant .config_entries import ConfigEntry , ConfigEntryState
1718from homeassistant .const import (
1819 EVENT_STATE_CHANGED ,
1920 STATE_CLOSED ,
2021 STATE_HOME ,
21- STATE_LOCKED ,
2222 STATE_NOT_HOME ,
2323 STATE_OFF ,
2424 STATE_ON ,
2525 STATE_OPEN ,
2626 STATE_UNKNOWN ,
27- STATE_UNLOCKED ,
2827)
29- from homeassistant .core import Event , EventStateChangedData , HomeAssistant , State , callback
30- from homeassistant .helpers import area_registry , device_registry , entity_registry , label_registry
28+ from homeassistant .core import (
29+ Event ,
30+ EventStateChangedData ,
31+ HomeAssistant ,
32+ State ,
33+ callback ,
34+ )
35+ from homeassistant .helpers import (
36+ area_registry ,
37+ device_registry ,
38+ entity_registry ,
39+ label_registry ,
40+ )
3141from homeassistant .helpers import state as state_helper
3242from homeassistant .util import dt as dt_util
3343from homeassistant .util .logging import async_create_catching_coro
6474 from custom_components .elasticsearch .es_gateway import ElasticsearchGateway
6575
6676ALLOWED_ATTRIBUTE_KEY_TYPES = str
67- ALLOWED_ATTRIBUTE_VALUE_TYPES = tuple | dict | set | list | int | float | bool | str | None
77+ ALLOWED_ATTRIBUTE_VALUE_TYPES = (
78+ tuple | dict | set | list | int | float | bool | str | None
79+ )
6880SKIP_ATTRIBUTES = [
6981 "friendly_name" ,
7082 "entity_picture" ,
@@ -193,7 +205,9 @@ async def async_init(self, config_entry: ConfigEntry) -> None:
193205 if len (self ._settings .change_detection_type ) != 0 :
194206 await self ._listener .async_init ()
195207 else :
196- self ._logger .warning ("No change detection type set. Disabling change listener." )
208+ self ._logger .warning (
209+ "No change detection type set. Disabling change listener."
210+ )
197211
198212 # We only need to initialize the poller if the user has configured a polling frequency
199213 if self ._settings .polling_frequency > 0 :
@@ -245,7 +259,10 @@ async def _populate_static_fields(self) -> None:
245259 if result .hostname :
246260 self ._static_fields ["host.hostname" ] = result .hostname
247261
248- if self ._hass .config .latitude is not None and self ._hass .config .longitude is not None :
262+ if (
263+ self ._hass .config .latitude is not None
264+ and self ._hass .config .longitude is not None
265+ ):
249266 self ._static_fields ["host.location" ] = [
250267 self ._hass .config .longitude ,
251268 self ._hass .config .latitude ,
@@ -258,9 +275,14 @@ async def _populate_static_fields(self) -> None:
258275 def reload_config_entry (self , msg ) -> None :
259276 """Reload the config entry."""
260277
261- if self ._config_entry and self ._config_entry .state == ConfigEntryState .LOADED :
278+ if (
279+ self ._config_entry
280+ and self ._config_entry .state == ConfigEntryState .LOADED
281+ ):
262282 self ._logger .info ("%s Reloading integration." , msg )
263- self ._hass .config_entries .async_schedule_reload (self ._config_entry .entry_id )
283+ self ._hass .config_entries .async_schedule_reload (
284+ self ._config_entry .entry_id
285+ )
264286 else :
265287 self ._logger .warning ("%s Config entry not found or not loaded." , msg )
266288
@@ -295,7 +317,9 @@ def __init__(
295317 self ._excluded_labels : list [str ] = settings .excluded_labels
296318 self ._included_entities : list [str ] = settings .included_entities
297319 self ._excluded_entities : list [str ] = settings .excluded_entities
298- self ._change_detection_type : list [StateChangeType ] = settings .change_detection_type
320+ self ._change_detection_type : list [StateChangeType ] = (
321+ settings .change_detection_type
322+ )
299323
300324 self ._entity_registry = entity_registry .async_get (hass )
301325 self ._label_registry = label_registry .async_get (hass )
@@ -325,66 +349,94 @@ def passes_filter(self, state: State, reason: StateChangeType) -> bool:
325349 if not self ._passes_change_detection_type_filter (reason ):
326350 return False
327351
328- entity : RegistryEntry | None = self ._entity_registry .async_get (state .entity_id )
352+ entity : RegistryEntry | None = self ._entity_registry .async_get (
353+ state .entity_id
354+ )
329355
330356 if not entity :
331357 return self ._reject (base_msg , "Entity not found in registry." )
332358
333359 device : DeviceEntry | None = (
334- self ._device_registry .async_get (entity .device_id ) if entity .device_id else None
360+ self ._device_registry .async_get (entity .device_id )
361+ if entity .device_id
362+ else None
335363 )
336364
337- if self ._exclude_targets and not self ._passes_exclude_targets (entity = entity , device = device ):
365+ if self ._exclude_targets and not self ._passes_exclude_targets (
366+ entity = entity , device = device
367+ ):
338368 return False
339369
340- if self ._include_targets and not self ._passes_include_targets (entity = entity , device = device ):
370+ if self ._include_targets and not self ._passes_include_targets (
371+ entity = entity , device = device
372+ ):
341373 return False
342374
343375 return self ._accept (base_msg , "Entity passed all filters." )
344376
345- def _passes_exclude_targets (self , entity : RegistryEntry , device : DeviceEntry | None ) -> bool :
377+ def _passes_exclude_targets (
378+ self , entity : RegistryEntry , device : DeviceEntry | None
379+ ) -> bool :
346380 base_msg = f"Processing exclusion filters for entity [{ entity .entity_id } ]: "
347381
348382 if entity .entity_id in self ._excluded_entities :
349383 return self ._reject (base_msg , "In the excluded entities list." )
350384
351385 if entity .area_id in self ._excluded_areas :
352- return self ._reject (base_msg , f"In an excluded area [{ entity .area_id } ]." )
386+ return self ._reject (
387+ base_msg , f"In an excluded area [{ entity .area_id } ]."
388+ )
353389
354390 for label in entity .labels :
355391 if label in self ._excluded_labels :
356- return self ._reject (base_msg , f"Excluded entity label present: [{ label } ]." )
392+ return self ._reject (
393+ base_msg , f"Excluded entity label present: [{ label } ]."
394+ )
357395
358396 if device is not None :
359397 if device .id in self ._excluded_devices :
360- return self ._reject (base_msg , f"Attached to an excluded device [{ device .id } ]." )
398+ return self ._reject (
399+ base_msg , f"Attached to an excluded device [{ device .id } ]."
400+ )
361401
362402 for label in device .labels :
363403 if label in self ._excluded_labels :
364- return self ._reject (base_msg , f"Excluded device label present: [{ label } ]." )
404+ return self ._reject (
405+ base_msg , f"Excluded device label present: [{ label } ]."
406+ )
365407
366408 return self ._accept (base_msg , "Entity was not excluded by filters." )
367409
368- def _passes_include_targets (self , entity : RegistryEntry , device : DeviceEntry | None ) -> bool :
410+ def _passes_include_targets (
411+ self , entity : RegistryEntry , device : DeviceEntry | None
412+ ) -> bool :
369413 base_msg = f"Processing inclusion filters for entity [{ entity .entity_id } ]: "
370414
371415 if entity .entity_id in self ._included_entities :
372416 return self ._accept (base_msg , "In the included entities list." )
373417
374418 if entity .area_id in self ._included_areas :
375- return self ._accept (base_msg , f"In an included area [{ entity .area_id } ]." )
419+ return self ._accept (
420+ base_msg , f"In an included area [{ entity .area_id } ]."
421+ )
376422
377423 for label in entity .labels :
378424 if label in self ._included_labels :
379- return self ._accept (base_msg , f"Included entity label present: [{ label } ]." )
425+ return self ._accept (
426+ base_msg , f"Included entity label present: [{ label } ]."
427+ )
380428
381429 if device is not None :
382430 if device .id in self ._included_devices :
383- return self ._accept (base_msg , f"Attached to an included device [{ device .id } ]." )
431+ return self ._accept (
432+ base_msg , f"Attached to an included device [{ device .id } ]."
433+ )
384434
385435 for label in device .labels :
386436 if label in self ._included_labels :
387- return self ._accept (base_msg , f"Included device label present: [{ label } ]." )
437+ return self ._accept (
438+ base_msg , f"Included device label present: [{ label } ]."
439+ )
388440
389441 return False
390442
@@ -504,7 +556,10 @@ class Formatter:
504556 """Formats state changes into documents."""
505557
506558 def __init__ (
507- self , hass : HomeAssistant , settings : PipelineSettings , log : Logger = BASE_LOGGER
559+ self ,
560+ hass : HomeAssistant ,
561+ settings : PipelineSettings ,
562+ log : Logger = BASE_LOGGER ,
508563 ) -> None :
509564 """Initialize the formatter."""
510565 self ._logger = log if log else BASE_LOGGER
@@ -519,14 +574,18 @@ async def async_init(self, static_fields: dict[str, Any]) -> None:
519574 """Initialize the formatter."""
520575 self ._static_fields = static_fields
521576
522- def format (self , time : datetime , state : State , reason : StateChangeType ) -> dict [str , Any ]:
577+ def format (
578+ self , time : datetime , state : State , reason : StateChangeType
579+ ) -> dict [str , Any ]:
523580 """Format the state change into a document."""
524581
525582 document = {
526583 "@timestamp" : time .isoformat (),
527584 "event.action" : reason .to_publish_reason (),
528585 "event.kind" : "event" ,
529- "event.type" : "info" if reason == StateChangeType .NO_CHANGE else "change" ,
586+ "event.type" : "info"
587+ if reason == StateChangeType .NO_CHANGE
588+ else "change" ,
530589 "hass.entity" : {** self ._state_to_extended_details (state )},
531590 "hass.entity.attributes" : self ._state_to_attributes (state ),
532591 "hass.entity.value" : state .state ,
@@ -541,13 +600,18 @@ def format(self, time: datetime, state: State, reason: StateChangeType) -> dict[
541600 def _state_to_extended_details (self , state : State ) -> dict :
542601 """Gather entity details from the state object and return a mapped dictionary ready to be put in an elasticsearch document."""
543602
544- document = self ._extended_entity_details .async_get (state .entity_id ).to_dict ()
603+ document = self ._extended_entity_details .async_get (
604+ state .entity_id
605+ ).to_dict ()
545606
546607 # The logic for friendly name is in the state for some reason
547608 document ["friendly_name" ] = state .name
548609
549610 if state .attributes .get ("longitude" ) and state .attributes .get ("latitude" ):
550- document ["location" ] = [state .attributes .get ("longitude" ), state .attributes .get ("latitude" )]
611+ document ["location" ] = [
612+ state .attributes .get ("longitude" ),
613+ state .attributes .get ("latitude" ),
614+ ]
551615
552616 return document
553617
@@ -621,7 +685,10 @@ def filter_attribute(self, entity_id, key, value) -> bool:
621685
622686 def reject (msg : str ) -> bool :
623687 if self ._debug_attribute_filtering :
624- message = f"Filtering attributes for entity [{ entity_id } ]: Attribute [{ key } ] " + msg
688+ message = (
689+ f"Filtering attributes for entity [{ entity_id } ]: Attribute [{ key } ] "
690+ + msg
691+ )
625692 self ._logger .debug (message )
626693
627694 return False
@@ -633,10 +700,14 @@ def reject(msg: str) -> bool:
633700 return reject (f"has a disallowed key type [{ type (key )} ]." )
634701
635702 if not isinstance (value , ALLOWED_ATTRIBUTE_VALUE_TYPES ):
636- return reject (f"with value [{ value } ] has disallowed value type [{ type (value )} ]." )
703+ return reject (
704+ f"with value [{ value } ] has disallowed value type [{ type (value )} ]."
705+ )
637706
638707 if key .strip () == "" :
639- return reject ("is empty after stripping leading and trailing whitespace." )
708+ return reject (
709+ "is empty after stripping leading and trailing whitespace."
710+ )
640711
641712 return True
642713
@@ -646,7 +717,9 @@ def normalize_attribute_name(attribute_name: str) -> str:
646717 """Create an ECS-compliant version of the provided attribute name."""
647718 # Normalize to closest ASCII equivalent where possible
648719 normalized_string = (
649- unicodedata .normalize ("NFKD" , attribute_name ).encode ("ascii" , "ignore" ).decode ()
720+ unicodedata .normalize ("NFKD" , attribute_name )
721+ .encode ("ascii" , "ignore" )
722+ .decode ()
650723 )
651724
652725 # Replace all non-word characters with an underscore
@@ -692,7 +765,7 @@ def state_as_boolean(cls, state: State) -> bool:
692765 if state .state in (
693766 "true" ,
694767 STATE_ON ,
695- STATE_LOCKED ,
768+ LockState . LOCKED ,
696769 STATE_ABOVE_HORIZON ,
697770 STATE_OPEN ,
698771 STATE_HOME ,
@@ -701,7 +774,7 @@ def state_as_boolean(cls, state: State) -> bool:
701774 if state .state in (
702775 "false" ,
703776 STATE_OFF ,
704- STATE_UNLOCKED ,
777+ LockState . UNLOCKED ,
705778 STATE_UNKNOWN ,
706779 STATE_BELOW_HORIZON ,
707780 STATE_CLOSED ,
@@ -795,10 +868,14 @@ async def publish(self) -> None:
795868
796869 try :
797870 if not await self ._gateway .check_connection ():
798- self ._logger .debug ("Skipping publishing as connection is not available." )
871+ self ._logger .debug (
872+ "Skipping publishing as connection is not available."
873+ )
799874 return
800875
801- actions = self ._add_action_and_meta_data (iterable = self ._manager .sip_queue ())
876+ actions = self ._add_action_and_meta_data (
877+ iterable = self ._manager .sip_queue ()
878+ )
802879
803880 await self ._gateway .bulk (actions = actions )
804881
0 commit comments