3535
3636TO_REDACT = {CONF_EMAIL , CONF_PASSWORD , CONF_SUBSCRIPTION_ID , CONF_MASTER_ID , CONF_MASTER_RELAY_ID }
3737
38+ # --- Global anonymization rules (applied to all endpoints) ---
39+
3840# Fields to fully replace with "***"
3941_REDACT_FIELDS = {"email" , "firstName" , "lastName" , "tenant" }
4042
4143# Fields to truncate to first 4 chars + "***"
42- _TRUNCATE_FIELDS = {"id" , "deviceId" , "sensorId" , "serialNumber" }
44+ _TRUNCATE_FIELDS = {"id" , "deviceId" , "device_id" , " sensorId" , "serialNumber" , "masterMac" , "mac " }
4345
4446# Fields to zero out
4547_ZERO_FIELDS = {"latitude" , "longitude" }
4648
4749# Fields to remove entirely
4850_REMOVE_FIELDS = {"authToken" }
4951
52+ # --- Per-endpoint extra redact fields ---
53+
54+ _PROFILE_REDACT_FIELDS = {
55+ "name" ,
56+ "birthDate" ,
57+ "postalCode" ,
58+ "city" ,
59+ "address" ,
60+ "additionalAddress" ,
61+ "phoneNumber" ,
62+ "mobileNumber" ,
63+ "maintainerCode" ,
64+ }
65+
66+ _DEVICES_REDACT_FIELDS : set [str ] = set ()
67+ _MEASURES_REDACT_FIELDS : set [str ] = set ()
68+ _STATES_REDACT_FIELDS : set [str ] = set ()
69+ _ROOMS_REDACT_FIELDS : set [str ] = set ()
70+
5071
51- def _anonymize_value (key : str , value : Any ) -> Any :
72+ def _anonymize_value (key : str , value : Any , extra_redact : set [ str ] ) -> Any :
5273 """Anonymize a single value based on its key."""
53- if key in _REDACT_FIELDS :
74+ if key in _REDACT_FIELDS or key in extra_redact :
5475 return "***"
5576 if key in _ZERO_FIELDS :
5677 return 0.0
@@ -59,34 +80,36 @@ def _anonymize_value(key: str, value: Any) -> Any:
5980 return value
6081
6182
62- def _anonymize_response (data : Any ) -> Any :
83+ def _anonymize_response (data : Any , extra_redact : set [ str ] | None = None ) -> Any :
6384 """Recursively anonymize sensitive fields in an API response."""
85+ redact = extra_redact or set ()
6486 if isinstance (data , dict ):
6587 result = {}
6688 for key , value in data .items ():
6789 if key in _REMOVE_FIELDS :
6890 continue
69- result [key ] = _anonymize_value (key , _anonymize_response (value ) )
91+ result [key ] = _anonymize_value (key , _anonymize_response (value , redact ), redact )
7092 return result
7193 if isinstance (data , list ):
72- return [_anonymize_response (item ) for item in data ]
94+ return [_anonymize_response (item , redact ) for item in data ]
7395 return data
7496
7597
76- # Each entry: (endpoint_path, param_builder)
98+ # Each entry: (endpoint_path, param_builder, extra_redact_fields )
7799# The callable receives (auth_token, entry_data, today, tomorrow) and returns params.
78- DiagnosticEndpoint = tuple [str , Callable [[str , dict , str , str ], dict ]]
100+ DiagnosticEndpoint = tuple [str , Callable [[str , dict , str , str ], dict ], set [ str ] ]
79101
80102DIAGNOSTIC_ENDPOINTS : list [DiagnosticEndpoint ] = [
81- (PROFILE_URL , lambda tok , data , t , tm : {"authToken" : tok }),
82- (DEVICES_URL , lambda tok , data , t , tm : {"authToken" : tok }),
103+ (PROFILE_URL , lambda tok , data , t , tm : {"authToken" : tok }, _PROFILE_REDACT_FIELDS ),
104+ (DEVICES_URL , lambda tok , data , t , tm : {"authToken" : tok }, _DEVICES_REDACT_FIELDS ),
83105 (
84106 MEASURES_TOTAL_URL ,
85107 lambda tok , data , t , tm : {
86108 "authToken" : tok ,
87109 "measureType" : data [CONF_GRID_TYPE ],
88110 "deviceId" : data [CONF_VIRTUAL_DEVICE_ID ],
89111 },
112+ _MEASURES_REDACT_FIELDS ,
90113 ),
91114 (
92115 MEASURES_GROUPING_URL ,
@@ -98,9 +121,10 @@ def _anonymize_response(data: Any) -> Any:
98121 "measureType" : data [CONF_GRID_TYPE ],
99122 "deviceId" : data [CONF_VIRTUAL_DEVICE_ID ],
100123 },
124+ _MEASURES_REDACT_FIELDS ,
101125 ),
102- (STATES_URL , lambda tok , data , t , tm : {"authToken" : tok }),
103- (ROOMS_URL , lambda tok , data , t , tm : {"authToken" : tok }),
126+ (STATES_URL , lambda tok , data , t , tm : {"authToken" : tok }, _STATES_REDACT_FIELDS ),
127+ (ROOMS_URL , lambda tok , data , t , tm : {"authToken" : tok }, _ROOMS_REDACT_FIELDS ),
104128]
105129
106130
@@ -128,18 +152,23 @@ async def async_get_config_entry_diagnostics(
128152 entry_data = dict (entry .data )
129153
130154 tasks = {
131- path : coordinator .client .async_raw_request (
132- "get" , path , params = param_builder (auth_token , entry_data , today , tomorrow )
155+ path : (
156+ coordinator .client .async_raw_request (
157+ "get" , path , params = param_builder (auth_token , entry_data , today , tomorrow )
158+ ),
159+ extra_redact ,
133160 )
134- for path , param_builder in DIAGNOSTIC_ENDPOINTS
161+ for path , param_builder , extra_redact in DIAGNOSTIC_ENDPOINTS
135162 }
136- results = await asyncio .gather (* tasks .values (), return_exceptions = True )
163+ results = await asyncio .gather (
164+ * [coro for coro , _ in tasks .values ()], return_exceptions = True
165+ )
137166
138- for path , result in zip (tasks .keys (), results ):
167+ for ( path , ( _ , extra_redact )), result in zip (tasks .items (), results ):
139168 if isinstance (result , Exception ):
140169 payload = {"error" : type (result ).__name__ , "message" : str (result )}
141170 else :
142- payload = _anonymize_response (result )
171+ payload = _anonymize_response (result , extra_redact )
143172 raw_api_responses [path ] = base64 .b64encode (
144173 json .dumps (payload , default = str ).encode ()
145174 ).decode ()
0 commit comments