Skip to content

Commit b82a963

Browse files
authored
Merge pull request #75 from RustyDust/more_global_data
Selects! Numbers! Buttons! ... and more ;)
2 parents aa7e236 + 96744a5 commit b82a963

File tree

13 files changed

+815
-382
lines changed

13 files changed

+815
-382
lines changed
Lines changed: 58 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,20 @@
11
"""The Sonnenbatterie integration."""
2-
3-
import json
2+
from datetime import timedelta
43

54
import homeassistant.helpers.config_validation as cv
65
import voluptuous as vol
76
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-
)
157
from homeassistant.core import (
16-
HomeAssistant,
17-
ServiceCall,
18-
ServiceResponse, SupportsResponse,
8+
HomeAssistant, SupportsResponse,
199
)
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
2711

2812
from .const import *
13+
from .coordinator import SonnenbatterieCoordinator
14+
from .sensor_list import SonnenbatterieSensorEntityDescription
15+
from .service import SonnenbatterieService
2916

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)
4318

4419
SCHEMA_SET_BATTERY_RESERVE = vol.Schema(
4520
{
@@ -70,16 +45,33 @@
7045
}
7146
)
7247

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+
7355
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
7873

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']
8375

8476
# noinspection PyPep8Naming
8577
SCHEMA_CHARGE_BATTERY = vol.Schema(
@@ -89,210 +81,84 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
8981
}
9082
)
9183

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)
14486

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
21890

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)
22793

22894
# service registration
22995
hass.services.async_register(
23096
DOMAIN,
23197
"charge_battery",
232-
charge_battery,
98+
services.charge_battery,
23399
schema=SCHEMA_CHARGE_BATTERY,
234-
supports_response=SupportsResponse.ONLY,
100+
supports_response=SupportsResponse.OPTIONAL,
235101
)
236102

237103
hass.services.async_register(
238104
DOMAIN,
239105
"discharge_battery",
240-
discharge_battery,
106+
services.discharge_battery,
241107
schema=SCHEMA_CHARGE_BATTERY,
242-
supports_response=SupportsResponse.ONLY,
108+
supports_response=SupportsResponse.OPTIONAL,
243109
)
244110

245111
hass.services.async_register(
246112
DOMAIN,
247113
"set_battery_reserve",
248-
set_battery_reserve,
114+
services.set_battery_reserve,
249115
schema=SCHEMA_SET_BATTERY_RESERVE,
250-
supports_response=SupportsResponse.ONLY,
116+
supports_response=SupportsResponse.OPTIONAL,
251117
)
252118

253119
hass.services.async_register(
254120
DOMAIN,
255121
"set_config_item",
256-
set_config_item,
122+
services.set_config_item,
257123
schema=SCHEMA_SET_CONFIG_ITEM,
258-
supports_response=SupportsResponse.ONLY,
124+
supports_response=SupportsResponse.OPTIONAL,
259125
)
260126

261127
hass.services.async_register(
262128
DOMAIN,
263129
"set_operating_mode",
264-
set_operating_mode,
130+
services.set_operating_mode,
265131
schema=SCHEMA_SET_OPERATING_MODE,
266-
supports_response=SupportsResponse.ONLY,
132+
supports_response=SupportsResponse.OPTIONAL,
267133
)
268134

269135
hass.services.async_register(
270136
DOMAIN,
271137
"set_tou_schedule",
272-
set_tou_schedule,
138+
services.set_tou_schedule,
273139
schema=SCHEMA_SET_TOU_SCHEDULE_STRING,
274-
supports_response=SupportsResponse.ONLY,
140+
supports_response=SupportsResponse.OPTIONAL,
275141
)
276142

277143
hass.services.async_register(
278144
DOMAIN,
279145
"get_tou_schedule",
280-
get_tou_schedule,
281-
supports_response=SupportsResponse.ONLY,
146+
services.get_tou_schedule,
147+
supports_response=SupportsResponse.OPTIONAL,
282148
)
283149

284150
hass.services.async_register(
285151
DOMAIN,
286152
"get_battery_reserve",
287-
get_battery_reserve,
288-
supports_response=SupportsResponse.ONLY,
153+
services.get_battery_reserve,
154+
supports_response=SupportsResponse.OPTIONAL,
289155
)
290156

291157
hass.services.async_register(
292158
DOMAIN,
293159
"get_operating_mode",
294-
get_operating_mode,
295-
supports_response=SupportsResponse.ONLY,
160+
services.get_operating_mode,
161+
supports_response=SupportsResponse.OPTIONAL,
296162
)
297163

298164
# Done setting up the entry
@@ -316,3 +182,4 @@ async def async_unload_entry(hass, entry):
316182
"""Handle removal of an entry."""
317183
LOGGER.debug(f"Unloading config entry: {entry}")
318184
return await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR)
185+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from homeassistant.components.button import ButtonEntity
2+
from homeassistant.config_entries import ConfigEntry
3+
from homeassistant.const import Platform
4+
from homeassistant.core import HomeAssistant
5+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
6+
7+
from . import SonnenbatterieCoordinator
8+
from .const import CONF_COORDINATOR, DOMAIN, LOGGER
9+
from .entities import SonnenButtonEntity, SonnenbatterieButtonEntityDescription, BUTTON_ENTITIES
10+
11+
12+
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
13+
LOGGER.debug(f"BUTTON async_setup_entry - {config_entry}")
14+
coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR]
15+
16+
entities = []
17+
for description in BUTTON_ENTITIES:
18+
if description.tag.type == Platform.BUTTON:
19+
entity = SonnenbatterieButton(coordinator, description)
20+
entities.append(entity)
21+
22+
async_add_entities(entities)
23+
24+
class SonnenbatterieButton(SonnenButtonEntity, ButtonEntity):
25+
26+
def __init__(self, coordinator: SonnenbatterieCoordinator, description: SonnenbatterieButtonEntityDescription):
27+
super().__init__(coordinator, description)
28+
29+
30+
async def async_press(self, **kwargs) -> None:
31+
tag = self.entity_description.tag
32+
match tag.key:
33+
case "button_reset_all":
34+
await self.coordinator.sbconn.sb2.charge_battery(0)
35+
await self.coordinator.sbconn.sb2.discharge_battery(0)
36+
case "button_reset_charge":
37+
await self.coordinator.sbconn.sb2.charge_battery(0)
38+
case "button_reset_discharge":
39+
await self.coordinator.sbconn.sb2.discharge_battery(0)
40+
41+
await self.coordinator.async_request_refresh()
42+
return None

0 commit comments

Comments
 (0)