Skip to content

Commit ddea220

Browse files
authored
Add start charge session action for blue current integration. (home-assistant#145446)
1 parent 32aacac commit ddea220

File tree

7 files changed

+284
-8
lines changed

7 files changed

+284
-8
lines changed

homeassistant/components/blue_current/__init__.py

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,55 @@
1313
RequestLimitReached,
1414
WebsocketError,
1515
)
16-
17-
from homeassistant.config_entries import ConfigEntry
18-
from homeassistant.const import CONF_API_TOKEN, Platform
19-
from homeassistant.core import HomeAssistant
20-
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
16+
import voluptuous as vol
17+
18+
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
19+
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
20+
from homeassistant.core import HomeAssistant, ServiceCall
21+
from homeassistant.exceptions import (
22+
ConfigEntryAuthFailed,
23+
ConfigEntryNotReady,
24+
ServiceValidationError,
25+
)
26+
from homeassistant.helpers import config_validation as cv, device_registry as dr
2127
from homeassistant.helpers.dispatcher import async_dispatcher_send
28+
from homeassistant.helpers.typing import ConfigType
2229

2330
from .const import (
31+
BCU_APP,
2432
CHARGEPOINT_SETTINGS,
2533
CHARGEPOINT_STATUS,
34+
CHARGING_CARD_ID,
2635
DOMAIN,
2736
EVSE_ID,
2837
LOGGER,
2938
PLUG_AND_CHARGE,
39+
SERVICE_START_CHARGE_SESSION,
3040
VALUE,
3141
)
3242

3343
type BlueCurrentConfigEntry = ConfigEntry[Connector]
3444

3545
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
3646
CHARGE_POINTS = "CHARGE_POINTS"
47+
CHARGE_CARDS = "CHARGE_CARDS"
3748
DATA = "data"
3849
DELAY = 5
3950

4051
GRID = "GRID"
4152
OBJECT = "object"
4253
VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
4354

55+
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
56+
57+
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
58+
{
59+
vol.Required(CONF_DEVICE_ID): cv.string,
60+
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
61+
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
62+
}
63+
)
64+
4465

4566
async def async_setup_entry(
4667
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
@@ -67,6 +88,66 @@ async def async_setup_entry(
6788
return True
6889

6990

91+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
92+
"""Set up Blue Current."""
93+
94+
async def start_charge_session(service_call: ServiceCall) -> None:
95+
"""Start a charge session with the provided device and charge card ID."""
96+
# When no charge card is provided, use the default charge card set in the config flow.
97+
charging_card_id = service_call.data[CHARGING_CARD_ID]
98+
device_id = service_call.data[CONF_DEVICE_ID]
99+
100+
# Get the device based on the given device ID.
101+
device = dr.async_get(hass).devices.get(device_id)
102+
103+
if device is None:
104+
raise ServiceValidationError(
105+
translation_domain=DOMAIN, translation_key="invalid_device_id"
106+
)
107+
108+
blue_current_config_entry: ConfigEntry | None = None
109+
110+
for config_entry_id in device.config_entries:
111+
config_entry = hass.config_entries.async_get_entry(config_entry_id)
112+
if not config_entry or config_entry.domain != DOMAIN:
113+
# Not the blue_current config entry.
114+
continue
115+
116+
if config_entry.state is not ConfigEntryState.LOADED:
117+
raise ServiceValidationError(
118+
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
119+
)
120+
121+
blue_current_config_entry = config_entry
122+
break
123+
124+
if not blue_current_config_entry:
125+
# The device is not connected to a valid blue_current config entry.
126+
raise ServiceValidationError(
127+
translation_domain=DOMAIN, translation_key="no_config_entry"
128+
)
129+
130+
connector = blue_current_config_entry.runtime_data
131+
132+
# Get the evse_id from the identifier of the device.
133+
evse_id = next(
134+
identifier[1]
135+
for identifier in device.identifiers
136+
if identifier[0] == DOMAIN
137+
)
138+
139+
await connector.client.start_session(evse_id, charging_card_id)
140+
141+
hass.services.async_register(
142+
DOMAIN,
143+
SERVICE_START_CHARGE_SESSION,
144+
start_charge_session,
145+
SERVICE_START_CHARGE_SESSION_SCHEMA,
146+
)
147+
148+
return True
149+
150+
70151
async def async_unload_entry(
71152
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
72153
) -> bool:
@@ -87,6 +168,7 @@ def __init__(
87168
self.client = client
88169
self.charge_points: dict[str, dict] = {}
89170
self.grid: dict[str, Any] = {}
171+
self.charge_cards: dict[str, dict[str, Any]] = {}
90172

91173
async def on_data(self, message: dict) -> None:
92174
"""Handle received data."""

homeassistant/components/blue_current/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
EVSE_ID = "evse_id"
1010
MODEL_TYPE = "model_type"
11+
CARD = "card"
12+
UID = "uid"
13+
BCU_APP = "BCU-APP"
14+
WITHOUT_CHARGING_CARD = "without_charging_card"
15+
CHARGING_CARD_ID = "charging_card_id"
16+
SERVICE_START_CHARGE_SESSION = "start_charge_session"
1117
PLUG_AND_CHARGE = "plug_and_charge"
1218
VALUE = "value"
1319
PERMISSION = "permission"

homeassistant/components/blue_current/icons.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,10 @@
4242
"default": "mdi:lock"
4343
}
4444
}
45+
},
46+
"services": {
47+
"start_charge_session": {
48+
"service": "mdi:play"
49+
}
4550
}
4651
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
start_charge_session:
2+
fields:
3+
device_id:
4+
selector:
5+
device:
6+
integration: blue_current
7+
required: true
8+
9+
charging_card_id:
10+
selector:
11+
text:
12+
required: false

homeassistant/components/blue_current/strings.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222
"wrong_account": "Wrong account: Please authenticate with the API token for {email}."
2323
}
2424
},
25+
"options": {
26+
"step": {
27+
"init": {
28+
"data": {
29+
"card": "Card"
30+
},
31+
"description": "Select the default charging card you want to use"
32+
}
33+
}
34+
},
2535
"entity": {
2636
"sensor": {
2737
"activity": {
@@ -136,5 +146,39 @@
136146
"name": "Block charge point"
137147
}
138148
}
149+
},
150+
"selector": {
151+
"select_charging_card": {
152+
"options": {
153+
"without_charging_card": "Without charging card"
154+
}
155+
}
156+
},
157+
"services": {
158+
"start_charge_session": {
159+
"name": "Start charge session",
160+
"description": "Starts a new charge session on a specified charge point.",
161+
"fields": {
162+
"charging_card_id": {
163+
"name": "Charging card ID",
164+
"description": "Optional charging card ID that will be used to start a charge session. When not provided, no charging card will be used."
165+
},
166+
"device_id": {
167+
"name": "Device ID",
168+
"description": "The ID of the Blue Current charge point."
169+
}
170+
}
171+
}
172+
},
173+
"exceptions": {
174+
"invalid_device_id": {
175+
"message": "Invalid device ID given."
176+
},
177+
"config_entry_not_loaded": {
178+
"message": "Config entry not loaded."
179+
},
180+
"no_config_entry": {
181+
"message": "Device has not a valid blue_current config entry."
182+
}
139183
}
140184
}

tests/components/blue_current/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from bluecurrent_api import Client
1111

1212
from homeassistant.components.blue_current import EVSE_ID, PLUG_AND_CHARGE
13-
from homeassistant.components.blue_current.const import PUBLIC_CHARGING
13+
from homeassistant.components.blue_current.const import PUBLIC_CHARGING, UID
14+
from homeassistant.const import CONF_ID
1415
from homeassistant.core import HomeAssistant
1516

1617
from tests.common import MockConfigEntry
@@ -87,6 +88,16 @@ async def get_grid_status(evse_id: str) -> None:
8788
"""Send the grid status to the callback."""
8889
await client_mock.receiver({"object": "GRID_STATUS", "data": grid})
8990

91+
async def get_charge_cards() -> None:
92+
"""Send the charge cards list to the callback."""
93+
await client_mock.receiver(
94+
{
95+
"object": "CHARGE_CARDS",
96+
"default_card": {UID: "BCU-APP", CONF_ID: "BCU-APP"},
97+
"cards": [{UID: "MOCK-CARD", CONF_ID: "MOCK-CARD", "valid": 1}],
98+
}
99+
)
100+
90101
async def update_charge_point(
91102
evse_id: str, event_object: str, settings: dict[str, Any]
92103
) -> None:
@@ -100,6 +111,7 @@ async def update_charge_point(
100111
client_mock.get_charge_points.side_effect = get_charge_points
101112
client_mock.get_status.side_effect = get_status
102113
client_mock.get_grid_status.side_effect = get_grid_status
114+
client_mock.get_charge_cards.side_effect = get_charge_cards
103115
client_mock.update_charge_point = update_charge_point
104116

105117
return client_mock

0 commit comments

Comments
 (0)