Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions homeassistant/components/analytics/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()


async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
async def async_devices_payload(hass: HomeAssistant) -> dict:
"""Return detailed information about entities and devices."""
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
Expand Down Expand Up @@ -538,6 +538,22 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
integration_input[1].append(entity_entry.entity_id)

integrations = {
domain: integration
for domain, integration in (
await async_get_integrations(hass, integration_inputs.keys())
).items()
if isinstance(integration, Integration)
}

# Filter out custom integrations
integration_inputs = {
domain: integration_info
for domain, integration_info in integration_inputs.items()
if (integration := integrations.get(domain)) is not None
and integration.is_built_in
}

# Call integrations that implement the analytics platform
for integration_domain, integration_input in integration_inputs.items():
if (
Expand Down Expand Up @@ -688,23 +704,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
else:
entities_info.append(entity_info)

integrations = {
domain: integration
for domain, integration in (
await async_get_integrations(hass, integrations_info.keys())
).items()
if isinstance(integration, Integration)
}

for domain, integration_info in integrations_info.items():
if integration := integrations.get(domain):
integration_info["is_custom_integration"] = not integration.is_built_in
# Include version for custom integrations
if not integration.is_built_in and integration.version:
integration_info["custom_integration_version"] = str(
integration.version
)

return {
"version": "home-assistant:1",
"home_assistant": HA_VERSION,
Expand Down
92 changes: 87 additions & 5 deletions homeassistant/components/blue_current/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,55 @@
RequestLimitReached,
WebsocketError,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType

from .const import (
BCU_APP,
CHARGEPOINT_SETTINGS,
CHARGEPOINT_STATUS,
CHARGING_CARD_ID,
DOMAIN,
EVSE_ID,
LOGGER,
PLUG_AND_CHARGE,
SERVICE_START_CHARGE_SESSION,
VALUE,
)

type BlueCurrentConfigEntry = ConfigEntry[Connector]

PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
CHARGE_POINTS = "CHARGE_POINTS"
CHARGE_CARDS = "CHARGE_CARDS"
DATA = "data"
DELAY = 5

GRID = "GRID"
OBJECT = "object"
VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)


async def async_setup_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
Expand All @@ -67,6 +88,66 @@ async def async_setup_entry(
return True


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""

async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]

# Get the device based on the given device ID.
device = dr.async_get(hass).devices.get(device_id)

if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)

blue_current_config_entry: ConfigEntry | None = None

for config_entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue

if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)

blue_current_config_entry = config_entry
break

if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)

connector = blue_current_config_entry.runtime_data

# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1]
for identifier in device.identifiers
if identifier[0] == DOMAIN
)

await connector.client.start_session(evse_id, charging_card_id)

hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)

return True


async def async_unload_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
) -> bool:
Expand All @@ -87,6 +168,7 @@ def __init__(
self.client = client
self.charge_points: dict[str, dict] = {}
self.grid: dict[str, Any] = {}
self.charge_cards: dict[str, dict[str, Any]] = {}

async def on_data(self, message: dict) -> None:
"""Handle received data."""
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/blue_current/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

EVSE_ID = "evse_id"
MODEL_TYPE = "model_type"
CARD = "card"
UID = "uid"
BCU_APP = "BCU-APP"
WITHOUT_CHARGING_CARD = "without_charging_card"
CHARGING_CARD_ID = "charging_card_id"
SERVICE_START_CHARGE_SESSION = "start_charge_session"
PLUG_AND_CHARGE = "plug_and_charge"
VALUE = "value"
PERMISSION = "permission"
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/blue_current/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@
"default": "mdi:lock"
}
}
},
"services": {
"start_charge_session": {
"service": "mdi:play"
}
}
}
12 changes: 12 additions & 0 deletions homeassistant/components/blue_current/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
start_charge_session:
fields:
device_id:
selector:
device:
integration: blue_current
required: true

charging_card_id:
selector:
text:
required: false
44 changes: 44 additions & 0 deletions homeassistant/components/blue_current/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"wrong_account": "Wrong account: Please authenticate with the API token for {email}."
}
},
"options": {
"step": {
"init": {
"data": {
"card": "Card"
},
"description": "Select the default charging card you want to use"
}
}
},
"entity": {
"sensor": {
"activity": {
Expand Down Expand Up @@ -136,5 +146,39 @@
"name": "Block charge point"
}
}
},
"selector": {
"select_charging_card": {
"options": {
"without_charging_card": "Without charging card"
}
}
},
"services": {
"start_charge_session": {
"name": "Start charge session",
"description": "Starts a new charge session on a specified charge point.",
"fields": {
"charging_card_id": {
"name": "Charging card ID",
"description": "Optional charging card ID that will be used to start a charge session. When not provided, no charging card will be used."
},
"device_id": {
"name": "Device ID",
"description": "The ID of the Blue Current charge point."
}
}
}
},
"exceptions": {
"invalid_device_id": {
"message": "Invalid device ID given."
},
"config_entry_not_loaded": {
"message": "Config entry not loaded."
},
"no_config_entry": {
"message": "Device has not a valid blue_current config entry."
}
}
}
10 changes: 7 additions & 3 deletions homeassistant/components/bluetooth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, cast

from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothScannerDevice,
Expand Down Expand Up @@ -38,13 +39,16 @@ def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:


@hass_callback
def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper:
"""Return a HaBleakScannerWrapper.
def async_get_scanner(hass: HomeAssistant) -> BleakScanner:
"""Return a HaBleakScannerWrapper cast to BleakScanner.

This is a wrapper around our BleakScanner singleton that allows
multiple integrations to share the same BleakScanner.

The wrapper is cast to BleakScanner for type compatibility with
libraries expecting a BleakScanner instance.
"""
return HaBleakScannerWrapper()
return cast(BleakScanner, HaBleakScannerWrapper())


@hass_callback
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/smartthings/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"low": "Low",
"low": "[%key:common::state::low%]",
"mid": "Mid",
"high": "High",
"high": "[%key:common::state::high%]",
"extra_high": "Extra high"
}
},
Expand Down Expand Up @@ -194,7 +194,7 @@
"state": {
"none": "None",
"heavy": "Heavy",
"normal": "Normal",
"normal": "[%key:common::state::normal%]",
"light": "Light",
"extra_light": "Extra light",
"extra_heavy": "Extra heavy",
Expand Down Expand Up @@ -626,7 +626,7 @@
"name": "Power freeze"
},
"auto_cycle_link": {
"name": "Auto cycle link"
"name": "Auto Cycle Link"
},
"sanitize": {
"name": "Sanitize"
Expand Down
22 changes: 0 additions & 22 deletions tests/components/analytics/test_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,25 +1121,6 @@ async def test_devices_payload_no_entities(
},
],
"entities": [],
"is_custom_integration": False,
},
"test": {
"devices": [
{
"entities": [],
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": "test-manufacturer7",
"model": None,
"model_id": "test-model-id7",
"sw_version": None,
"via_device": None,
},
],
"entities": [],
"is_custom_integration": True,
"custom_integration_version": "1.2.3",
},
},
}
Expand Down Expand Up @@ -1299,7 +1280,6 @@ async def test_devices_payload_with_entities(
"unit_of_measurement": "°C",
},
],
"is_custom_integration": False,
},
"template": {
"devices": [],
Expand All @@ -1315,7 +1295,6 @@ async def test_devices_payload_with_entities(
"unit_of_measurement": None,
},
],
"is_custom_integration": False,
},
},
}
Expand Down Expand Up @@ -1429,7 +1408,6 @@ async def async_modify_analytics(
"unit_of_measurement": None,
},
],
"is_custom_integration": False,
},
},
}
Loading
Loading