1
1
"""The Sonnenbatterie integration."""
2
-
3
- import json
2
+ from datetime import timedelta
4
3
5
4
import homeassistant .helpers .config_validation as cv
6
5
import voluptuous as vol
7
6
from homeassistant .config_entries import ConfigEntry
8
- from homeassistant .const import (
9
- Platform ,
10
- ATTR_DEVICE_ID ,
11
- CONF_IP_ADDRESS ,
12
- CONF_PASSWORD ,
13
- CONF_USERNAME ,
14
- )
15
7
from homeassistant .core import (
16
- HomeAssistant ,
17
- ServiceCall ,
18
- ServiceResponse , SupportsResponse ,
8
+ HomeAssistant , SupportsResponse ,
19
9
)
20
- from homeassistant .exceptions import HomeAssistantError
21
- from homeassistant .helpers .device_registry import (
22
- async_get as dr_async_get ,
23
- )
24
- from homeassistant .util .read_only_dict import ReadOnlyDict
25
- from sonnenbatterie import AsyncSonnenBatterie
26
- from timeofuse import TimeofUseSchedule
10
+ from homeassistant .exceptions import ConfigEntryNotReady
27
11
28
12
from .const import *
13
+ from .coordinator import SonnenbatterieCoordinator
14
+ from .sensor_list import SonnenbatterieSensorEntityDescription
15
+ from .service import SonnenbatterieService
29
16
30
- # rustydust_241227: this doesn't seem to be needed - kept until we're sure ;)
31
- # async def async_setup(hass, config):
32
- # """Set up a skeleton component."""
33
- # hass.data.setdefault(DOMAIN, {})
34
- # return True
35
-
36
- SB_OPERATING_MODES = {
37
- "manual" : 1 ,
38
- "automatic" : 2 ,
39
- "expansion" : 6 ,
40
- "timeofuse" : 10
41
- }
42
-
17
+ SCAN_INTERVAL = timedelta (seconds = DEFAULT_SCAN_INTERVAL )
43
18
44
19
SCHEMA_SET_BATTERY_RESERVE = vol .Schema (
45
20
{
70
45
}
71
46
)
72
47
48
+
49
+ # noinspection PyUnusedLocal
50
+ async def async_setup (hass , config ):
51
+ """Set up using YAML is not supported by this integration."""
52
+ return True
53
+
54
+
73
55
async def async_setup_entry (hass : HomeAssistant , config_entry : ConfigEntry ) -> bool :
74
- LOGGER .debug (f"setup_entry: { config_entry .data } \n { config_entry .entry_id } " )
75
- ip_address = config_entry .data [CONF_IP_ADDRESS ]
76
- password = config_entry .data [CONF_PASSWORD ]
77
- username = config_entry .data [CONF_USERNAME ]
56
+ LOGGER .debug (f"setup_entry: { config_entry .data } | { config_entry .entry_id } " )
57
+ # only initialize if not already present
58
+ if DOMAIN not in hass .data :
59
+ hass .data .setdefault (DOMAIN , {})
60
+
61
+ # init the master coordinator
62
+
63
+ sb_coordinator = SonnenbatterieCoordinator (hass , config_entry )
64
+ # calls SonnenbatterieCoordinator._async_update_data()
65
+ await sb_coordinator .async_refresh ()
66
+ if not sb_coordinator .last_update_success :
67
+ raise ConfigEntryNotReady
68
+ else :
69
+ await sb_coordinator .fetch_sonnenbatterie_on_startup ()
70
+ # save coordinator as early as possible
71
+ hass .data [DOMAIN ][config_entry .entry_id ] = {}
72
+ hass .data [DOMAIN ][config_entry .entry_id ][CONF_COORDINATOR ] = sb_coordinator
78
73
79
- sb = AsyncSonnenBatterie (username , password , ip_address )
80
- await sb .login ()
81
- inverter_power = int ((await sb .get_batterysystem ())['battery_system' ]['system' ]['inverter_capacity' ])
82
- await sb .logout ()
74
+ inverter_power = sb_coordinator .latestData ['battery_system' ]['battery_system' ]['system' ]['inverter_capacity' ]
83
75
84
76
# noinspection PyPep8Naming
85
77
SCHEMA_CHARGE_BATTERY = vol .Schema (
@@ -89,210 +81,84 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
89
81
}
90
82
)
91
83
92
- # Set up base data in hass object
93
- hass .data .setdefault (DOMAIN , {})
94
- hass .data [DOMAIN ][config_entry .entry_id ] = {}
95
- hass .data [DOMAIN ][config_entry .entry_id ][CONF_IP_ADDRESS ] = ip_address
96
- hass .data [DOMAIN ][config_entry .entry_id ][CONF_USERNAME ] = username
97
- hass .data [DOMAIN ][config_entry .entry_id ][CONF_PASSWORD ] = password
98
-
99
- await hass .config_entries .async_forward_entry_setups (config_entry , [ Platform .SENSOR ])
100
- # rustydust_241227: this doesn't seem to be needed
101
- # config_entry.add_update_listener(update_listener)
102
- # config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
103
-
104
- def _get_sb_connection (call_data : ReadOnlyDict ) -> AsyncSonnenBatterie :
105
- LOGGER .debug (f"_get_sb_connection: { call_data } " )
106
- if ATTR_DEVICE_ID in call_data :
107
- # no idea why, but sometimes it's a list and other times a str
108
- if isinstance (call_data [ATTR_DEVICE_ID ], list ):
109
- device_id = call_data [ATTR_DEVICE_ID ][0 ]
110
- else :
111
- device_id = call_data [ATTR_DEVICE_ID ]
112
- device_registry = dr_async_get (hass )
113
- if not (device_entry := device_registry .async_get (device_id )):
114
- raise HomeAssistantError (f"No device found for device_id: { device_id } " )
115
- if not (sb_config := hass .data [DOMAIN ][device_entry .primary_config_entry ]):
116
- raise HomeAssistantError (f"Unable to find config for device_id: { device_id } ({ device_entry .name } )" )
117
- if not (sb_config .get (CONF_USERNAME ) and sb_config .get (CONF_PASSWORD ) and sb_config .get (CONF_IP_ADDRESS )):
118
- raise HomeAssistantError (f"Invalid config for device_id: { device_id } ({ sb_config } ). Please report an issue at { SONNENBATTERIE_ISSUE_URL } ." )
119
- return AsyncSonnenBatterie (sb_config .get (CONF_USERNAME ), sb_config .get (CONF_PASSWORD ), sb_config .get (CONF_IP_ADDRESS ))
120
- else :
121
- return sb
122
-
123
- # service definitions
124
- async def charge_battery (call : ServiceCall ) -> ServiceResponse :
125
- power = int (call .data .get (CONF_CHARGE_WATT ))
126
- # Make sure we have an sb2 object
127
- sb_conn = _get_sb_connection (call .data )
128
- await sb_conn .login ()
129
- response = await sb_conn .sb2 .charge_battery (power )
130
- await sb_conn .logout ()
131
- return {
132
- "charge" : response ,
133
- }
134
-
135
- async def discharge_battery (call : ServiceCall ) -> ServiceResponse :
136
- power = int (call .data .get (CONF_CHARGE_WATT ))
137
- sb_conn = _get_sb_connection (call .data )
138
- await sb_conn .login ()
139
- response = await sb_conn .sb2 .discharge_battery (power )
140
- await sb_conn .logout ()
141
- return {
142
- "discharge" : response ,
143
- }
84
+ # Initialize our services
85
+ services = SonnenbatterieService (hass , config_entry , sb_coordinator )
144
86
145
- async def set_battery_reserve (call : ServiceCall ) -> ServiceResponse :
146
- value = call .data .get (CONF_SERVICE_VALUE )
147
- sb_conn = _get_sb_connection (call .data )
148
- await sb_conn .login ()
149
- response = int ((await sb_conn .sb2 .set_battery_reserve (value ))["EM_USOC" ])
150
- await sb_conn .logout ()
151
- return {
152
- "battery_reserve" : response ,
153
- }
154
-
155
- async def set_config_item (call : ServiceCall ) -> ServiceResponse :
156
- item = call .data .get (CONF_SERVICE_ITEM )
157
- value = call .data .get (CONF_SERVICE_VALUE )
158
- sb_conn = _get_sb_connection (call .data )
159
- await sb_conn .login ()
160
- response = await sb_conn .sb2 .set_config_item (item , value )
161
- await sb_conn .logout ()
162
- return {
163
- "response" : response ,
164
- }
165
-
166
- async def set_operating_mode (call : ServiceCall ) -> ServiceResponse :
167
- mode = SB_OPERATING_MODES .get (call .data .get ('mode' ))
168
- sb_conn = _get_sb_connection (call .data )
169
- await sb_conn .login ()
170
- response = await sb_conn .set_operating_mode (mode )
171
- await sb_conn .logout ()
172
- return {
173
- "mode" : response ,
174
- }
175
-
176
- async def set_tou_schedule (call : ServiceCall ) -> ServiceResponse :
177
- schedule = call .data .get (CONF_SERVICE_SCHEDULE )
178
- try :
179
- json_schedule = json .loads (schedule )
180
- except ValueError as e :
181
- raise HomeAssistantError (f"Schedule is not a valid JSON string: '{ schedule } '" ) from e
182
-
183
- tou = TimeofUseSchedule ()
184
- try :
185
- tou .load_tou_schedule_from_json (json_schedule )
186
- except ValueError as e :
187
- raise HomeAssistantError (f"Schedule is not a valid schedule: '{ schedule } '" ) from e
188
- except TypeError as t :
189
- raise HomeAssistantError (f"Schedule is not a valid schedule: '{ schedule } '" ) from t
190
-
191
- sb_conn = _get_sb_connection (call .data )
192
- await sb_conn .login ()
193
- response = await sb_conn .sb2 .set_tou_schedule_string (schedule )
194
- await sb_conn .logout ()
195
- return {
196
- "schedule" : response ["EM_ToU_Schedule" ],
197
- }
198
-
199
- # noinspection PyUnusedLocal
200
- async def get_tou_schedule (call : ServiceCall ) -> ServiceResponse :
201
- sb_conn = _get_sb_connection (call .data )
202
- await sb_conn .login ()
203
- response = await sb_conn .sb2 .get_tou_schedule_string ()
204
- await sb_conn .logout ()
205
- return {
206
- "schedule" : response ,
207
- }
208
-
209
- # noinspection PyUnusedLocal
210
- async def get_battery_reserve (call : ServiceCall ) -> ServiceResponse :
211
- sb_conn = _get_sb_connection (call .data )
212
- await sb_conn .login ()
213
- response = await sb_conn .sb2 .get_battery_reserve ()
214
- await sb_conn .logout ()
215
- return {
216
- "backup_reserve" : response ,
217
- }
87
+ # Set up base data in hass object
88
+ # hass.data.setdefault(DOMAIN, {})
89
+ hass .data [DOMAIN ][config_entry .entry_id ][CONF_INVERTER_MAX ] = inverter_power
218
90
219
- async def get_operating_mode (call : ServiceCall ) -> ServiceResponse :
220
- sb_conn = _get_sb_connection (call .data )
221
- await sb_conn .login ()
222
- response = await sb_conn .sb2 .get_operating_mode ()
223
- await sb_conn .logout ()
224
- return {
225
- "operating_mode" : response ,
226
- }
91
+ # Setup our sensors, services and whatnot
92
+ await hass .config_entries .async_forward_entry_setups (config_entry , PLATFORMS )
227
93
228
94
# service registration
229
95
hass .services .async_register (
230
96
DOMAIN ,
231
97
"charge_battery" ,
232
- charge_battery ,
98
+ services . charge_battery ,
233
99
schema = SCHEMA_CHARGE_BATTERY ,
234
- supports_response = SupportsResponse .ONLY ,
100
+ supports_response = SupportsResponse .OPTIONAL ,
235
101
)
236
102
237
103
hass .services .async_register (
238
104
DOMAIN ,
239
105
"discharge_battery" ,
240
- discharge_battery ,
106
+ services . discharge_battery ,
241
107
schema = SCHEMA_CHARGE_BATTERY ,
242
- supports_response = SupportsResponse .ONLY ,
108
+ supports_response = SupportsResponse .OPTIONAL ,
243
109
)
244
110
245
111
hass .services .async_register (
246
112
DOMAIN ,
247
113
"set_battery_reserve" ,
248
- set_battery_reserve ,
114
+ services . set_battery_reserve ,
249
115
schema = SCHEMA_SET_BATTERY_RESERVE ,
250
- supports_response = SupportsResponse .ONLY ,
116
+ supports_response = SupportsResponse .OPTIONAL ,
251
117
)
252
118
253
119
hass .services .async_register (
254
120
DOMAIN ,
255
121
"set_config_item" ,
256
- set_config_item ,
122
+ services . set_config_item ,
257
123
schema = SCHEMA_SET_CONFIG_ITEM ,
258
- supports_response = SupportsResponse .ONLY ,
124
+ supports_response = SupportsResponse .OPTIONAL ,
259
125
)
260
126
261
127
hass .services .async_register (
262
128
DOMAIN ,
263
129
"set_operating_mode" ,
264
- set_operating_mode ,
130
+ services . set_operating_mode ,
265
131
schema = SCHEMA_SET_OPERATING_MODE ,
266
- supports_response = SupportsResponse .ONLY ,
132
+ supports_response = SupportsResponse .OPTIONAL ,
267
133
)
268
134
269
135
hass .services .async_register (
270
136
DOMAIN ,
271
137
"set_tou_schedule" ,
272
- set_tou_schedule ,
138
+ services . set_tou_schedule ,
273
139
schema = SCHEMA_SET_TOU_SCHEDULE_STRING ,
274
- supports_response = SupportsResponse .ONLY ,
140
+ supports_response = SupportsResponse .OPTIONAL ,
275
141
)
276
142
277
143
hass .services .async_register (
278
144
DOMAIN ,
279
145
"get_tou_schedule" ,
280
- get_tou_schedule ,
281
- supports_response = SupportsResponse .ONLY ,
146
+ services . get_tou_schedule ,
147
+ supports_response = SupportsResponse .OPTIONAL ,
282
148
)
283
149
284
150
hass .services .async_register (
285
151
DOMAIN ,
286
152
"get_battery_reserve" ,
287
- get_battery_reserve ,
288
- supports_response = SupportsResponse .ONLY ,
153
+ services . get_battery_reserve ,
154
+ supports_response = SupportsResponse .OPTIONAL ,
289
155
)
290
156
291
157
hass .services .async_register (
292
158
DOMAIN ,
293
159
"get_operating_mode" ,
294
- get_operating_mode ,
295
- supports_response = SupportsResponse .ONLY ,
160
+ services . get_operating_mode ,
161
+ supports_response = SupportsResponse .OPTIONAL ,
296
162
)
297
163
298
164
# Done setting up the entry
@@ -316,3 +182,4 @@ async def async_unload_entry(hass, entry):
316
182
"""Handle removal of an entry."""
317
183
LOGGER .debug (f"Unloading config entry: { entry } " )
318
184
return await hass .config_entries .async_forward_entry_unload (entry , Platform .SENSOR )
185
+
0 commit comments