mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Add switches for blue current integration. (#146210)
This commit is contained in:
parent
6d3872252b
commit
1c8ae8a21b
@ -15,23 +15,31 @@ from bluecurrent_api.exceptions import (
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform
|
||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
|
||||
from .const import (
|
||||
CHARGEPOINT_SETTINGS,
|
||||
CHARGEPOINT_STATUS,
|
||||
DOMAIN,
|
||||
EVSE_ID,
|
||||
LOGGER,
|
||||
PLUG_AND_CHARGE,
|
||||
VALUE,
|
||||
)
|
||||
|
||||
type BlueCurrentConfigEntry = ConfigEntry[Connector]
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
|
||||
CHARGE_POINTS = "CHARGE_POINTS"
|
||||
DATA = "data"
|
||||
DELAY = 5
|
||||
|
||||
GRID = "GRID"
|
||||
OBJECT = "object"
|
||||
VALUE_TYPES = ["CH_STATUS"]
|
||||
VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -94,7 +102,7 @@ class Connector:
|
||||
elif object_name in VALUE_TYPES:
|
||||
value_data: dict = message[DATA]
|
||||
evse_id = value_data.pop(EVSE_ID)
|
||||
self.update_charge_point(evse_id, value_data)
|
||||
self.update_charge_point(evse_id, object_name, value_data)
|
||||
|
||||
# gets grid key / values
|
||||
elif GRID in object_name:
|
||||
@ -106,26 +114,37 @@ class Connector:
|
||||
"""Handle incoming chargepoint data."""
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.handle_charge_point(
|
||||
entry[EVSE_ID], entry[MODEL_TYPE], entry[ATTR_NAME]
|
||||
)
|
||||
self.handle_charge_point(entry[EVSE_ID], entry)
|
||||
for entry in charge_points_data
|
||||
),
|
||||
self.client.get_grid_status(charge_points_data[0][EVSE_ID]),
|
||||
)
|
||||
|
||||
async def handle_charge_point(self, evse_id: str, model: str, name: str) -> None:
|
||||
async def handle_charge_point(
|
||||
self, evse_id: str, charge_point: dict[str, Any]
|
||||
) -> None:
|
||||
"""Add the chargepoint and request their data."""
|
||||
self.add_charge_point(evse_id, model, name)
|
||||
self.add_charge_point(evse_id, charge_point)
|
||||
await self.client.get_status(evse_id)
|
||||
|
||||
def add_charge_point(self, evse_id: str, model: str, name: str) -> None:
|
||||
def add_charge_point(self, evse_id: str, charge_point: dict[str, Any]) -> None:
|
||||
"""Add a charge point to charge_points."""
|
||||
self.charge_points[evse_id] = {MODEL_TYPE: model, ATTR_NAME: name}
|
||||
self.charge_points[evse_id] = charge_point
|
||||
|
||||
def update_charge_point(self, evse_id: str, data: dict) -> None:
|
||||
def update_charge_point(self, evse_id: str, update_type: str, data: dict) -> None:
|
||||
"""Update the charge point data."""
|
||||
self.charge_points[evse_id].update(data)
|
||||
charge_point = self.charge_points[evse_id]
|
||||
if update_type == CHARGEPOINT_SETTINGS:
|
||||
# Update the plug and charge object. The library parses this object to a bool instead of an object.
|
||||
plug_and_charge = charge_point.get(PLUG_AND_CHARGE)
|
||||
if plug_and_charge is not None:
|
||||
plug_and_charge[VALUE] = data[PLUG_AND_CHARGE]
|
||||
|
||||
# Remove the plug and charge object from the data list before updating.
|
||||
del data[PLUG_AND_CHARGE]
|
||||
|
||||
charge_point.update(data)
|
||||
|
||||
self.dispatch_charge_point_update_signal(evse_id)
|
||||
|
||||
def dispatch_charge_point_update_signal(self, evse_id: str) -> None:
|
||||
|
@ -8,3 +8,14 @@ LOGGER = logging.getLogger(__package__)
|
||||
|
||||
EVSE_ID = "evse_id"
|
||||
MODEL_TYPE = "model_type"
|
||||
PLUG_AND_CHARGE = "plug_and_charge"
|
||||
VALUE = "value"
|
||||
PERMISSION = "permission"
|
||||
CHARGEPOINT_STATUS = "CH_STATUS"
|
||||
CHARGEPOINT_SETTINGS = "CH_SETTINGS"
|
||||
BLOCK = "block"
|
||||
UNAVAILABLE = "unavailable"
|
||||
AVAILABLE = "available"
|
||||
LINKED_CHARGE_CARDS = "linked_charge_cards_only"
|
||||
PUBLIC_CHARGING = "public_charging"
|
||||
ACTIVITY = "activity"
|
||||
|
@ -30,6 +30,17 @@
|
||||
"stop_charge_session": {
|
||||
"default": "mdi:stop"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"plug_and_charge": {
|
||||
"default": "mdi:ev-plug-type2"
|
||||
},
|
||||
"linked_charge_cards": {
|
||||
"default": "mdi:account-group"
|
||||
},
|
||||
"block": {
|
||||
"default": "mdi:lock"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,17 @@
|
||||
"reset": {
|
||||
"name": "Reset"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"plug_and_charge": {
|
||||
"name": "Plug & Charge"
|
||||
},
|
||||
"linked_charge_cards_only": {
|
||||
"name": "Linked charging cards only"
|
||||
},
|
||||
"block": {
|
||||
"name": "Block charge point"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
169
homeassistant/components/blue_current/switch.py
Normal file
169
homeassistant/components/blue_current/switch.py
Normal file
@ -0,0 +1,169 @@
|
||||
"""Support for Blue Current switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PLUG_AND_CHARGE, BlueCurrentConfigEntry, Connector
|
||||
from .const import (
|
||||
AVAILABLE,
|
||||
BLOCK,
|
||||
LINKED_CHARGE_CARDS,
|
||||
PUBLIC_CHARGING,
|
||||
UNAVAILABLE,
|
||||
VALUE,
|
||||
)
|
||||
from .entity import ChargepointEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class BlueCurrentSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes a Blue Current switch entity."""
|
||||
|
||||
function: Callable[[Connector, str, bool], Any]
|
||||
|
||||
turn_on_off_fn: Callable[[str, Connector], tuple[bool, bool]]
|
||||
"""Update the switch based on the latest data received from the websocket. The first returned boolean is _attr_is_on, the second one has_value."""
|
||||
|
||||
|
||||
def update_on_value_and_activity(
|
||||
key: str, evse_id: str, connector: Connector, reverse_is_on: bool = False
|
||||
) -> tuple[bool, bool]:
|
||||
"""Return the updated state of the switch based on received chargepoint data and activity."""
|
||||
|
||||
data_object = connector.charge_points[evse_id].get(key)
|
||||
is_on = data_object[VALUE] if data_object is not None else None
|
||||
activity = connector.charge_points[evse_id].get("activity")
|
||||
|
||||
if is_on is not None and activity == AVAILABLE:
|
||||
return is_on if not reverse_is_on else not is_on, True
|
||||
return False, False
|
||||
|
||||
|
||||
def update_block_switch(evse_id: str, connector: Connector) -> tuple[bool, bool]:
|
||||
"""Return the updated data for a block switch."""
|
||||
activity = connector.charge_points[evse_id].get("activity")
|
||||
return activity == UNAVAILABLE, activity in [AVAILABLE, UNAVAILABLE]
|
||||
|
||||
|
||||
def update_charge_point(
|
||||
key: str, evse_id: str, connector: Connector, new_switch_value: bool
|
||||
) -> None:
|
||||
"""Change charge point data when the state of the switch changes."""
|
||||
data_objects = connector.charge_points[evse_id].get(key)
|
||||
if data_objects is not None:
|
||||
data_objects[VALUE] = new_switch_value
|
||||
|
||||
|
||||
async def set_plug_and_charge(connector: Connector, evse_id: str, value: bool) -> None:
|
||||
"""Toggle the plug and charge setting for a specific charging point."""
|
||||
await connector.client.set_plug_and_charge(evse_id, value)
|
||||
update_charge_point(PLUG_AND_CHARGE, evse_id, connector, value)
|
||||
|
||||
|
||||
async def set_linked_charge_cards(
|
||||
connector: Connector, evse_id: str, value: bool
|
||||
) -> None:
|
||||
"""Toggle the plug and charge setting for a specific charging point."""
|
||||
await connector.client.set_linked_charge_cards_only(evse_id, value)
|
||||
update_charge_point(PUBLIC_CHARGING, evse_id, connector, not value)
|
||||
|
||||
|
||||
SWITCHES = (
|
||||
BlueCurrentSwitchEntityDescription(
|
||||
key=PLUG_AND_CHARGE,
|
||||
translation_key=PLUG_AND_CHARGE,
|
||||
function=set_plug_and_charge,
|
||||
turn_on_off_fn=lambda evse_id, connector: (
|
||||
update_on_value_and_activity(PLUG_AND_CHARGE, evse_id, connector)
|
||||
),
|
||||
),
|
||||
BlueCurrentSwitchEntityDescription(
|
||||
key=LINKED_CHARGE_CARDS,
|
||||
translation_key=LINKED_CHARGE_CARDS,
|
||||
function=set_linked_charge_cards,
|
||||
turn_on_off_fn=lambda evse_id, connector: (
|
||||
update_on_value_and_activity(
|
||||
PUBLIC_CHARGING, evse_id, connector, reverse_is_on=True
|
||||
)
|
||||
),
|
||||
),
|
||||
BlueCurrentSwitchEntityDescription(
|
||||
key=BLOCK,
|
||||
translation_key=BLOCK,
|
||||
function=lambda connector, evse_id, value: connector.client.block(
|
||||
evse_id, value
|
||||
),
|
||||
turn_on_off_fn=update_block_switch,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BlueCurrentConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Blue Current switches."""
|
||||
connector = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
ChargePointSwitch(
|
||||
connector,
|
||||
evse_id,
|
||||
switch,
|
||||
)
|
||||
for evse_id in connector.charge_points
|
||||
for switch in SWITCHES
|
||||
)
|
||||
|
||||
|
||||
class ChargePointSwitch(ChargepointEntity, SwitchEntity):
|
||||
"""Base charge point switch."""
|
||||
|
||||
has_value = True
|
||||
entity_description: BlueCurrentSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connector: Connector,
|
||||
evse_id: str,
|
||||
switch: BlueCurrentSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(connector, evse_id)
|
||||
|
||||
self.key = switch.key
|
||||
self.entity_description = switch
|
||||
self.evse_id = evse_id
|
||||
self._attr_available = True
|
||||
self._attr_unique_id = f"{switch.key}_{evse_id}"
|
||||
|
||||
async def call_function(self, value: bool) -> None:
|
||||
"""Call the function to set setting."""
|
||||
await self.entity_description.function(self.connector, self.evse_id, value)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.call_function(True)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.call_function(False)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the switch."""
|
||||
new_state = self.entity_description.turn_on_off_fn(self.evse_id, self.connector)
|
||||
self._attr_is_on = new_state[0]
|
||||
self.has_value = new_state[1]
|
@ -4,18 +4,28 @@ from __future__ import annotations
|
||||
|
||||
from asyncio import Event, Future
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from bluecurrent_api import Client
|
||||
|
||||
from homeassistant.components.blue_current import EVSE_ID, PLUG_AND_CHARGE
|
||||
from homeassistant.components.blue_current.const import PUBLIC_CHARGING
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DEFAULT_CHARGE_POINT_OPTIONS = {
|
||||
PLUG_AND_CHARGE: {"value": False, "permission": "write"},
|
||||
PUBLIC_CHARGING: {"value": True, "permission": "write"},
|
||||
}
|
||||
|
||||
DEFAULT_CHARGE_POINT = {
|
||||
"evse_id": "101",
|
||||
"model_type": "",
|
||||
"name": "",
|
||||
"activity": "available",
|
||||
**DEFAULT_CHARGE_POINT_OPTIONS,
|
||||
}
|
||||
|
||||
|
||||
@ -77,11 +87,20 @@ def create_client_mock(
|
||||
"""Send the grid status to the callback."""
|
||||
await client_mock.receiver({"object": "GRID_STATUS", "data": grid})
|
||||
|
||||
async def update_charge_point(
|
||||
evse_id: str, event_object: str, settings: dict[str, Any]
|
||||
) -> None:
|
||||
"""Update the charge point data by sending an event."""
|
||||
await client_mock.receiver(
|
||||
{"object": event_object, "data": {EVSE_ID: evse_id, **settings}}
|
||||
)
|
||||
|
||||
client_mock.connect.side_effect = connect
|
||||
client_mock.wait_for_charge_points.side_effect = wait_for_charge_points
|
||||
client_mock.get_charge_points.side_effect = get_charge_points
|
||||
client_mock.get_status.side_effect = get_status
|
||||
client_mock.get_grid_status.side_effect = get_grid_status
|
||||
client_mock.update_charge_point = update_charge_point
|
||||
|
||||
return client_mock
|
||||
|
||||
|
@ -7,17 +7,10 @@ import pytest
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
from . import DEFAULT_CHARGE_POINT, init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
charge_point = {
|
||||
"evse_id": "101",
|
||||
"model_type": "",
|
||||
"name": "",
|
||||
}
|
||||
|
||||
|
||||
charge_point_status = {
|
||||
"actual_v1": 14,
|
||||
"actual_v2": 18,
|
||||
@ -97,7 +90,7 @@ async def test_sensors_created(
|
||||
hass,
|
||||
config_entry,
|
||||
"sensor",
|
||||
charge_point,
|
||||
DEFAULT_CHARGE_POINT,
|
||||
charge_point_status | charge_point_status_timestamps,
|
||||
grid,
|
||||
)
|
||||
@ -116,7 +109,7 @@ async def test_sensors(
|
||||
) -> None:
|
||||
"""Test the underlying sensors."""
|
||||
await init_integration(
|
||||
hass, config_entry, "sensor", charge_point, charge_point_status, grid
|
||||
hass, config_entry, "sensor", DEFAULT_CHARGE_POINT, charge_point_status, grid
|
||||
)
|
||||
|
||||
for entity_id, key in charge_point_entity_ids.items():
|
||||
|
152
tests/components/blue_current/test_switch.py
Normal file
152
tests/components/blue_current/test_switch.py
Normal file
@ -0,0 +1,152 @@
|
||||
"""The tests for Bluecurrent switches."""
|
||||
|
||||
from homeassistant.components.blue_current import CHARGEPOINT_SETTINGS, PLUG_AND_CHARGE
|
||||
from homeassistant.components.blue_current.const import (
|
||||
ACTIVITY,
|
||||
CHARGEPOINT_STATUS,
|
||||
PUBLIC_CHARGING,
|
||||
UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
|
||||
from . import DEFAULT_CHARGE_POINT, init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_switches(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: EntityRegistry
|
||||
) -> None:
|
||||
"""Test the underlying switches."""
|
||||
|
||||
await init_integration(hass, config_entry, Platform.SWITCH)
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for switch in entity_entries:
|
||||
state = hass.states.get(switch.entity_id)
|
||||
|
||||
assert state and state.state == STATE_OFF
|
||||
entry = entity_registry.async_get(switch.entity_id)
|
||||
assert entry and entry.unique_id == switch.unique_id
|
||||
|
||||
|
||||
async def test_switches_offline(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: EntityRegistry
|
||||
) -> None:
|
||||
"""Test if switches are disabled when needed."""
|
||||
charge_point = DEFAULT_CHARGE_POINT.copy()
|
||||
charge_point[ACTIVITY] = "offline"
|
||||
|
||||
await init_integration(hass, config_entry, Platform.SWITCH, charge_point)
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for switch in entity_entries:
|
||||
state = hass.states.get(switch.entity_id)
|
||||
|
||||
assert state and state.state == UNAVAILABLE
|
||||
entry = entity_registry.async_get(switch.entity_id)
|
||||
assert entry and entry.entity_id == switch.entity_id
|
||||
|
||||
|
||||
async def test_block_switch_availability(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: EntityRegistry
|
||||
) -> None:
|
||||
"""Test if the block switch is unavailable when charging."""
|
||||
charge_point = DEFAULT_CHARGE_POINT.copy()
|
||||
charge_point[ACTIVITY] = "charging"
|
||||
|
||||
await init_integration(hass, config_entry, Platform.SWITCH, charge_point)
|
||||
|
||||
state = hass.states.get("switch.101_block_charge_point")
|
||||
assert state and state.state == UNAVAILABLE
|
||||
|
||||
|
||||
async def test_toggle(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: EntityRegistry
|
||||
) -> None:
|
||||
"""Test the on / off methods and if the switch gets updated."""
|
||||
await init_integration(hass, config_entry, Platform.SWITCH)
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for switch in entity_entries:
|
||||
state = hass.states.get(switch.entity_id)
|
||||
|
||||
assert state and state.state == STATE_OFF
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": switch.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(switch.entity_id)
|
||||
assert state and state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
{"entity_id": switch.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(switch.entity_id)
|
||||
assert state and state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_setting_change(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: EntityRegistry
|
||||
) -> None:
|
||||
"""Test if the state of the switches are updated when an update message from the websocket comes in."""
|
||||
integration = await init_integration(hass, config_entry, Platform.SWITCH)
|
||||
client_mock = integration[0]
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for switch in entity_entries:
|
||||
state = hass.states.get(switch.entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await client_mock.update_charge_point(
|
||||
"101",
|
||||
CHARGEPOINT_SETTINGS,
|
||||
{
|
||||
PLUG_AND_CHARGE: True,
|
||||
PUBLIC_CHARGING: {"value": False, "permission": "write"},
|
||||
},
|
||||
)
|
||||
|
||||
charge_cards_only_switch = hass.states.get("switch.101_linked_charging_cards_only")
|
||||
assert charge_cards_only_switch.state == STATE_ON
|
||||
|
||||
plug_and_charge_switch = hass.states.get("switch.101_plug_charge")
|
||||
assert plug_and_charge_switch.state == STATE_ON
|
||||
|
||||
plug_and_charge_switch = hass.states.get("switch.101_block_charge_point")
|
||||
assert plug_and_charge_switch.state == STATE_OFF
|
||||
|
||||
await client_mock.update_charge_point(
|
||||
"101", CHARGEPOINT_STATUS, {ACTIVITY: UNAVAILABLE}
|
||||
)
|
||||
|
||||
charge_cards_only_switch = hass.states.get("switch.101_linked_charging_cards_only")
|
||||
assert charge_cards_only_switch.state == STATE_UNAVAILABLE
|
||||
|
||||
plug_and_charge_switch = hass.states.get("switch.101_plug_charge")
|
||||
assert plug_and_charge_switch.state == STATE_UNAVAILABLE
|
||||
|
||||
switch = hass.states.get("switch.101_block_charge_point")
|
||||
assert switch.state == STATE_ON
|
Loading…
x
Reference in New Issue
Block a user