mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 08:29:39 +02:00
Compare commits
1 Commits
epenet-202
...
media-sour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d184037f5a |
@@ -13,30 +13,20 @@ from bluecurrent_api.exceptions import (
|
||||
RequestLimitReached,
|
||||
WebsocketError,
|
||||
)
|
||||
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.config_entries import ConfigEntry
|
||||
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 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,
|
||||
)
|
||||
|
||||
@@ -44,7 +34,6 @@ type BlueCurrentConfigEntry = ConfigEntry[Connector]
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
|
||||
CHARGE_POINTS = "CHARGE_POINTS"
|
||||
CHARGE_CARDS = "CHARGE_CARDS"
|
||||
DATA = "data"
|
||||
DELAY = 5
|
||||
|
||||
@@ -52,16 +41,6 @@ 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
|
||||
@@ -88,66 +67,6 @@ 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:
|
||||
@@ -168,7 +87,6 @@ class Connector:
|
||||
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."""
|
||||
|
||||
@@ -8,12 +8,6 @@ LOGGER = logging.getLogger(__package__)
|
||||
|
||||
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"
|
||||
|
||||
@@ -42,10 +42,5 @@
|
||||
"default": "mdi:lock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_charge_session": {
|
||||
"service": "mdi:play"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
start_charge_session:
|
||||
fields:
|
||||
device_id:
|
||||
selector:
|
||||
device:
|
||||
integration: blue_current
|
||||
required: true
|
||||
|
||||
charging_card_id:
|
||||
selector:
|
||||
text:
|
||||
required: false
|
||||
@@ -22,16 +22,6 @@
|
||||
"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": {
|
||||
@@ -146,39 +136,5 @@
|
||||
"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,7 +10,6 @@ from asyncio import Future
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from bleak import BleakScanner
|
||||
from habluetooth import (
|
||||
BaseHaScanner,
|
||||
BluetoothScannerDevice,
|
||||
@@ -39,16 +38,13 @@ def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_get_scanner(hass: HomeAssistant) -> BleakScanner:
|
||||
"""Return a HaBleakScannerWrapper cast to BleakScanner.
|
||||
def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper:
|
||||
"""Return a HaBleakScannerWrapper.
|
||||
|
||||
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 cast(BleakScanner, HaBleakScannerWrapper())
|
||||
return HaBleakScannerWrapper()
|
||||
|
||||
|
||||
@hass_callback
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.integration_platform import (
|
||||
@@ -73,7 +72,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
# Local sources support
|
||||
await _process_media_source_platform(hass, DOMAIN, local_source)
|
||||
hass.http.register_view(local_source.UploadMediaView)
|
||||
websocket_api.async_register_command(hass, local_source.websocket_remove_media)
|
||||
|
||||
await async_process_integration_platforms(
|
||||
hass, DOMAIN, _process_media_source_platform
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -16,14 +17,19 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.components.websocket_api import ActiveConnection
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import MEDIA_SOURCE_DATA
|
||||
from .error import Unresolvable
|
||||
from .helper import async_browse_media, async_resolve_media
|
||||
from .models import MediaSourceItem
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the HTTP views and WebSocket commands for media sources."""
|
||||
websocket_api.async_register_command(hass, websocket_browse_media)
|
||||
websocket_api.async_register_command(hass, websocket_resolve_media)
|
||||
websocket_api.async_register_command(hass, websocket_remove_media)
|
||||
frontend.async_register_built_in_panel(
|
||||
hass, "media-browser", "media_browser", "hass:play-box-multiple"
|
||||
)
|
||||
@@ -77,3 +83,46 @@ async def websocket_resolve_media(
|
||||
"mime_type": media.mime_type,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "media_source/remove_media",
|
||||
vol.Required("media_content_id"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_remove_media(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Remove media."""
|
||||
try:
|
||||
item = MediaSourceItem.from_uri(hass, msg["media_content_id"], None)
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
|
||||
if item.domain is None:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_INVALID_FORMAT,
|
||||
"Media source domain required",
|
||||
)
|
||||
return
|
||||
|
||||
source = hass.data[MEDIA_SOURCE_DATA][item.domain]
|
||||
|
||||
try:
|
||||
await source.async_delete_media(item)
|
||||
except NotImplementedError:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.ERR_NOT_SUPPORTED, "Delete not supported"
|
||||
)
|
||||
except Unresolvable as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, str(err))
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected error removing media")
|
||||
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -7,13 +7,13 @@ import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any, Protocol, cast
|
||||
from typing import Protocol, cast
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import FileField
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http, websocket_api
|
||||
from homeassistant.components import http
|
||||
from homeassistant.components.http import require_admin
|
||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -28,10 +28,6 @@ MAX_UPLOAD_SIZE = 1024 * 1024 * 10
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PathNotSupportedError(HomeAssistantError):
|
||||
"""Error to indicate a path is not supported."""
|
||||
|
||||
|
||||
class InvalidFileNameError(HomeAssistantError):
|
||||
"""Error to indicate an invalid file name."""
|
||||
|
||||
@@ -102,10 +98,10 @@ class LocalSource(MediaSource):
|
||||
|
||||
def _do_delete() -> None:
|
||||
if not item_path.exists():
|
||||
raise FileNotFoundError("Path does not exist")
|
||||
raise Unresolvable("Path does not exist")
|
||||
|
||||
if not item_path.is_file():
|
||||
raise PathNotSupportedError("Path is not a file")
|
||||
raise Unresolvable("Path is not a file")
|
||||
|
||||
item_path.unlink()
|
||||
|
||||
@@ -141,7 +137,7 @@ class LocalSource(MediaSource):
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
except ValueError as err:
|
||||
raise PathNotSupportedError("Invalid path") from err
|
||||
raise Unresolvable("Invalid path") from err
|
||||
|
||||
with target_path.open("wb") as target_fp:
|
||||
shutil.copyfileobj(uploaded_file.file, target_fp)
|
||||
@@ -380,51 +376,8 @@ class UploadMediaView(http.HomeAssistantView):
|
||||
except InvalidFileNameError as err:
|
||||
LOGGER.error("Invalid filename uploaded: %s", data["file"].filename)
|
||||
raise web.HTTPBadRequest from err
|
||||
except PathNotSupportedError as err:
|
||||
LOGGER.error("Invalid path for upload: %s", data["media_content_id"])
|
||||
raise web.HTTPBadRequest from err
|
||||
except OSError as err:
|
||||
LOGGER.error("Error uploading file: %s", err)
|
||||
raise web.HTTPInternalServerError from err
|
||||
|
||||
return self.json({"media_content_id": uploaded_media_source_id})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "media_source/local_source/remove",
|
||||
vol.Required("media_content_id"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_remove_media(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Remove media."""
|
||||
try:
|
||||
item = MediaSourceItem.from_uri(hass, msg["media_content_id"], None)
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
|
||||
if item.domain != DOMAIN:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.ERR_INVALID_FORMAT, "Invalid media source domain"
|
||||
)
|
||||
return
|
||||
|
||||
source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][item.domain])
|
||||
|
||||
try:
|
||||
await source.async_delete_media(item)
|
||||
except Unresolvable as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
except FileNotFoundError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, str(err))
|
||||
except PathNotSupportedError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_SUPPORTED, str(err))
|
||||
except OSError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -27,7 +27,12 @@ class BrowseMediaSource(BrowseMedia):
|
||||
"""Represent a browsable media file."""
|
||||
|
||||
def __init__(
|
||||
self, *, domain: str | None, identifier: str | None, **kwargs: Any
|
||||
self,
|
||||
*,
|
||||
domain: str | None,
|
||||
identifier: str | None,
|
||||
can_delete: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize media source browse media."""
|
||||
media_content_id = f"{URI_SCHEME}{domain or ''}"
|
||||
@@ -38,6 +43,13 @@ class BrowseMediaSource(BrowseMedia):
|
||||
|
||||
self.domain = domain
|
||||
self.identifier = identifier
|
||||
self.can_delete = can_delete
|
||||
|
||||
def as_dict(self, *, parent: bool = True) -> dict[str, Any]:
|
||||
"""Convert BrowseMediaSource to a dictionary."""
|
||||
response = super().as_dict(parent=parent)
|
||||
response["can_delete"] = self.can_delete
|
||||
return response
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -135,3 +147,7 @@ class MediaSource:
|
||||
async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
|
||||
"""Browse media."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_delete_media(self, item: MediaSourceItem) -> None:
|
||||
"""Delete media."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -158,6 +158,7 @@ DEFAULT_STRUCTURE_PREFIX = ">f"
|
||||
DEFAULT_TEMP_UNIT = "C"
|
||||
DEFAULT_HVAC_ON_VALUE = 1
|
||||
DEFAULT_HVAC_OFF_VALUE = 0
|
||||
MODBUS_DOMAIN = "modbus"
|
||||
DOMAIN = "modbus"
|
||||
|
||||
ACTIVE_SCAN_INTERVAL = 2 # limit to force an extra update
|
||||
|
||||
@@ -10,8 +10,7 @@ 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, UID
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.components.blue_current.const import PUBLIC_CHARGING
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -88,16 +87,6 @@ def create_client_mock(
|
||||
"""Send the grid status to the callback."""
|
||||
await client_mock.receiver({"object": "GRID_STATUS", "data": grid})
|
||||
|
||||
async def get_charge_cards() -> None:
|
||||
"""Send the charge cards list to the callback."""
|
||||
await client_mock.receiver(
|
||||
{
|
||||
"object": "CHARGE_CARDS",
|
||||
"default_card": {UID: "BCU-APP", CONF_ID: "BCU-APP"},
|
||||
"cards": [{UID: "MOCK-CARD", CONF_ID: "MOCK-CARD", "valid": 1}],
|
||||
}
|
||||
)
|
||||
|
||||
async def update_charge_point(
|
||||
evse_id: str, event_object: str, settings: dict[str, Any]
|
||||
) -> None:
|
||||
@@ -111,7 +100,6 @@ def create_client_mock(
|
||||
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.get_charge_cards.side_effect = get_charge_cards
|
||||
client_mock.update_charge_point = update_charge_point
|
||||
|
||||
return client_mock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Test Blue Current Init Component."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from bluecurrent_api.exceptions import (
|
||||
BlueCurrentException,
|
||||
@@ -10,24 +10,15 @@ from bluecurrent_api.exceptions import (
|
||||
WebsocketError,
|
||||
)
|
||||
import pytest
|
||||
from voluptuous import MultipleInvalid
|
||||
|
||||
from homeassistant.components.blue_current import (
|
||||
CHARGING_CARD_ID,
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
async_setup_entry,
|
||||
)
|
||||
from homeassistant.components.blue_current import async_setup_entry
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
IntegrationError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
|
||||
from . import init_integration
|
||||
|
||||
@@ -41,7 +32,6 @@ async def test_load_unload_entry(
|
||||
with (
|
||||
patch("homeassistant.components.blue_current.Client.validate_api_token"),
|
||||
patch("homeassistant.components.blue_current.Client.wait_for_charge_points"),
|
||||
patch("homeassistant.components.blue_current.Client.get_charge_cards"),
|
||||
patch("homeassistant.components.blue_current.Client.disconnect"),
|
||||
patch(
|
||||
"homeassistant.components.blue_current.Client.connect",
|
||||
@@ -113,108 +103,3 @@ async def test_connect_request_limit_reached_error(
|
||||
await started_loop.wait()
|
||||
assert mock_client.get_next_reset_delta.call_count == 1
|
||||
assert mock_client.connect.call_count == 2
|
||||
|
||||
|
||||
async def test_start_charging_action(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: DeviceRegistry
|
||||
) -> None:
|
||||
"""Test the start charing action when a charging card is provided."""
|
||||
integration = await init_integration(hass, config_entry, Platform.BUTTON)
|
||||
client = integration[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{
|
||||
CONF_DEVICE_ID: list(device_registry.devices)[0],
|
||||
CHARGING_CARD_ID: "TEST_CARD",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
client.start_session.assert_called_once_with("101", "TEST_CARD")
|
||||
|
||||
|
||||
async def test_start_charging_action_without_card(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: DeviceRegistry
|
||||
) -> None:
|
||||
"""Test the start charing action when no charging card is provided."""
|
||||
integration = await init_integration(hass, config_entry, Platform.BUTTON)
|
||||
client = integration[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{
|
||||
CONF_DEVICE_ID: list(device_registry.devices)[0],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
client.start_session.assert_called_once_with("101", "BCU-APP")
|
||||
|
||||
|
||||
async def test_start_charging_action_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
device_registry: DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test the start charing action errors."""
|
||||
await init_integration(hass, config_entry, Platform.BUTTON)
|
||||
|
||||
with pytest.raises(MultipleInvalid):
|
||||
# No device id
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
# Invalid device id
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{CONF_DEVICE_ID: "INVALID"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test when the device is not connected to a valid blue_current config entry.
|
||||
get_entry_mock = MagicMock()
|
||||
get_entry_mock.state = ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
hass.config_entries, "async_get_entry", return_value=get_entry_mock
|
||||
),
|
||||
pytest.raises(ServiceValidationError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{
|
||||
CONF_DEVICE_ID: list(device_registry.devices)[0],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test when the blue_current config entry is not loaded.
|
||||
get_entry_mock = MagicMock()
|
||||
get_entry_mock.domain = DOMAIN
|
||||
get_entry_mock.state = ConfigEntryState.NOT_LOADED
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
hass.config_entries, "async_get_entry", return_value=get_entry_mock
|
||||
),
|
||||
pytest.raises(ServiceValidationError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_CHARGE_SESSION,
|
||||
{
|
||||
CONF_DEVICE_ID: list(device_registry.devices)[0],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -297,7 +297,7 @@ async def test_remove_file(
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msgid(),
|
||||
"type": "media_source/local_source/remove",
|
||||
"type": "media_source/remove_media",
|
||||
"media_content_id": f"media-source://media_source/test_dir/{to_delete.name}",
|
||||
}
|
||||
)
|
||||
@@ -317,23 +317,21 @@ async def test_remove_file(
|
||||
websocket_api.ERR_NOT_FOUND,
|
||||
),
|
||||
# Only a dir
|
||||
("media-source://media_source/test_dir", websocket_api.ERR_NOT_SUPPORTED),
|
||||
("media-source://media_source/test_dir", websocket_api.ERR_NOT_FOUND),
|
||||
# File with extra identifiers
|
||||
(
|
||||
f"media-source://media_source/test_dir/bla/../{extra_id_file.name}",
|
||||
websocket_api.ERR_INVALID_FORMAT,
|
||||
websocket_api.ERR_NOT_FOUND,
|
||||
),
|
||||
# Location is invalid
|
||||
("media-source://media_source/test_dir/..", websocket_api.ERR_INVALID_FORMAT),
|
||||
# Domain != media_source
|
||||
("media-source://nest/test_dir/.", websocket_api.ERR_INVALID_FORMAT),
|
||||
("media-source://media_source/test_dir/..", websocket_api.ERR_NOT_FOUND),
|
||||
# Completely something else
|
||||
("http://bla", websocket_api.ERR_INVALID_FORMAT),
|
||||
):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msgid(),
|
||||
"type": "media_source/local_source/remove",
|
||||
"type": "media_source/remove_media",
|
||||
"media_content_id": bad_id,
|
||||
}
|
||||
)
|
||||
@@ -341,7 +339,7 @@ async def test_remove_file(
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not msg["success"], bad_id
|
||||
assert msg["error"]["code"] == err
|
||||
assert msg["error"]["code"] == err, bad_id
|
||||
|
||||
assert extra_id_file.exists()
|
||||
|
||||
@@ -352,7 +350,7 @@ async def test_remove_file(
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msgid(),
|
||||
"type": "media_source/local_source/remove",
|
||||
"type": "media_source/remove_media",
|
||||
"media_content_id": f"media-source://media_source/test_dir/{to_delete_2.name}",
|
||||
}
|
||||
)
|
||||
@@ -369,7 +367,7 @@ async def test_remove_file(
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msgid(),
|
||||
"type": "media_source/local_source/remove",
|
||||
"type": "media_source/remove_media",
|
||||
"media_content_id": f"media-source://media_source/test_dir/{to_delete_3.name}",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -56,10 +56,14 @@ async def test_trigger_subtype(hass: HomeAssistant) -> None:
|
||||
assert integration_mock.call_args == call(hass, "test")
|
||||
|
||||
|
||||
async def test_trigger_variables(
|
||||
async def test_trigger_variables(hass: HomeAssistant) -> None:
|
||||
"""Test trigger variables."""
|
||||
|
||||
|
||||
async def test_if_fires_on_event(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test trigger variables."""
|
||||
"""Test the firing of events."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"automation",
|
||||
|
||||
Reference in New Issue
Block a user