diff --git a/.coveragerc b/.coveragerc index 83555abc974..b21e4d9d7f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1585,10 +1585,6 @@ omit = homeassistant/components/vesync/sensor.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py - homeassistant/components/viam/__init__.py - homeassistant/components/viam/const.py - homeassistant/components/viam/manager.py - homeassistant/components/viam/services.py homeassistant/components/vicare/__init__.py homeassistant/components/vicare/button.py homeassistant/components/vicare/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 46476fac7c7..d4bcc363e58 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1523,8 +1523,6 @@ build.json @home-assistant/supervisor /tests/components/version/ @ludeeus /homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja /tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja -/homeassistant/components/viam/ @hipsterbrown -/tests/components/viam/ @hipsterbrown /homeassistant/components/vicare/ @CFenner /tests/components/vicare/ @CFenner /homeassistant/components/vilfo/ @ManneW diff --git a/homeassistant/components/viam/__init__.py b/homeassistant/components/viam/__init__.py deleted file mode 100644 index 924e3a544fe..00000000000 --- a/homeassistant/components/viam/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -"""The viam integration.""" - -from __future__ import annotations - -from viam.app.viam_client import ViamClient -from viam.rpc.dial import Credentials, DialOptions - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_API_KEY -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_API_ID, - CONF_CREDENTIAL_TYPE, - CONF_SECRET, - CRED_TYPE_API_KEY, - DOMAIN, -) -from .manager import ViamManager -from .services import async_setup_services - -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Viam services.""" - - async_setup_services(hass) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up viam from a config entry.""" - credential_type = entry.data[CONF_CREDENTIAL_TYPE] - payload = entry.data[CONF_SECRET] - auth_entity = entry.data[CONF_ADDRESS] - if credential_type == CRED_TYPE_API_KEY: - payload = entry.data[CONF_API_KEY] - auth_entity = entry.data[CONF_API_ID] - - credentials = Credentials(type=credential_type, payload=payload) - dial_options = DialOptions(auth_entity=auth_entity, credentials=credentials) - viam_client = await ViamClient.create_from_dial_options(dial_options=dial_options) - manager = ViamManager(hass, viam_client, entry.entry_id, dict(entry.data)) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = manager - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - manager: ViamManager = hass.data[DOMAIN].pop(entry.entry_id) - manager.unload() - - return True diff --git a/homeassistant/components/viam/config_flow.py b/homeassistant/components/viam/config_flow.py deleted file mode 100644 index 5afa00769e3..00000000000 --- a/homeassistant/components/viam/config_flow.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Config flow for viam integration.""" - -from __future__ import annotations - -import logging -from typing import Any - -from viam.app.viam_client import ViamClient -from viam.rpc.dial import Credentials, DialOptions -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_ADDRESS, CONF_API_KEY -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.selector import ( - SelectOptionDict, - SelectSelector, - SelectSelectorConfig, -) - -from .const import ( - CONF_API_ID, - CONF_CREDENTIAL_TYPE, - CONF_ROBOT, - CONF_ROBOT_ID, - CONF_SECRET, - CRED_TYPE_API_KEY, - CRED_TYPE_LOCATION_SECRET, - DOMAIN, -) - -_LOGGER = logging.getLogger(__name__) - - -STEP_AUTH_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_CREDENTIAL_TYPE): SelectSelector( - SelectSelectorConfig( - options=[ - CRED_TYPE_API_KEY, - CRED_TYPE_LOCATION_SECRET, - ], - translation_key=CONF_CREDENTIAL_TYPE, - ) - ) - } -) -STEP_AUTH_ROBOT_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_ADDRESS): str, - vol.Required(CONF_SECRET): str, - } -) -STEP_AUTH_ORG_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_ID): str, - vol.Required(CONF_API_KEY): str, - } -) - - -async def validate_input(data: dict[str, Any]) -> tuple[str, ViamClient]: - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - credential_type = data[CONF_CREDENTIAL_TYPE] - auth_entity = data.get(CONF_API_ID) - secret = data.get(CONF_API_KEY) - if credential_type == CRED_TYPE_LOCATION_SECRET: - auth_entity = data.get(CONF_ADDRESS) - secret = data.get(CONF_SECRET) - - if not secret: - raise CannotConnect - - creds = Credentials(type=credential_type, payload=secret) - opts = DialOptions(auth_entity=auth_entity, credentials=creds) - client = await ViamClient.create_from_dial_options(opts) - - # If you cannot connect: - # throw CannotConnect - if client: - locations = await client.app_client.list_locations() - location = await client.app_client.get_location(next(iter(locations)).id) - - # Return info that you want to store in the config entry. - return (location.name, client) - - raise CannotConnect - - -class ViamFlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a config flow for viam.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize.""" - self._title = "" - self._client: ViamClient - self._data: dict[str, Any] = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" - errors: dict[str, str] = {} - if user_input is not None: - self._data.update(user_input) - - if self._data.get(CONF_CREDENTIAL_TYPE) == CRED_TYPE_API_KEY: - return await self.async_step_auth_api_key() - - return await self.async_step_auth_robot_location() - - return self.async_show_form( - step_id="user", data_schema=STEP_AUTH_USER_DATA_SCHEMA, errors=errors - ) - - async def async_step_auth_api_key( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the API Key authentication.""" - errors = await self.__handle_auth_input(user_input) - if errors is None: - return await self.async_step_robot() - - return self.async_show_form( - step_id="auth_api_key", - data_schema=STEP_AUTH_ORG_DATA_SCHEMA, - errors=errors, - ) - - async def async_step_auth_robot_location( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the robot location authentication.""" - errors = await self.__handle_auth_input(user_input) - if errors is None: - return await self.async_step_robot() - - return self.async_show_form( - step_id="auth_robot_location", - data_schema=STEP_AUTH_ROBOT_DATA_SCHEMA, - errors=errors, - ) - - async def async_step_robot( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Select robot from location.""" - if user_input is not None: - self._data.update({CONF_ROBOT_ID: user_input[CONF_ROBOT]}) - return self.async_create_entry(title=self._title, data=self._data) - - app_client = self._client.app_client - locations = await app_client.list_locations() - robots = await app_client.list_robots(next(iter(locations)).id) - - return self.async_show_form( - step_id="robot", - data_schema=vol.Schema( - { - vol.Required(CONF_ROBOT): SelectSelector( - SelectSelectorConfig( - options=[ - SelectOptionDict(value=robot.id, label=robot.name) - for robot in robots - ] - ) - ) - } - ), - ) - - @callback - def async_remove(self) -> None: - """Notification that the flow has been removed.""" - if self._client is not None: - self._client.close() - - async def __handle_auth_input( - self, user_input: dict[str, Any] | None = None - ) -> dict[str, str] | None: - """Validate user input for the common authentication logic. - - Returns: - A dictionary with any handled errors if any occurred, or None - - """ - errors: dict[str, str] | None = None - if user_input is not None: - try: - self._data.update(user_input) - (title, client) = await validate_input(self._data) - self._title = title - self._client = client - except CannotConnect: - errors = {"base": "cannot_connect"} - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors = {"base": "unknown"} - else: - errors = {} - - return errors - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/viam/const.py b/homeassistant/components/viam/const.py deleted file mode 100644 index 9cf4932d04e..00000000000 --- a/homeassistant/components/viam/const.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Constants for the viam integration.""" - -DOMAIN = "viam" - -CONF_API_ID = "api_id" -CONF_SECRET = "secret" -CONF_CREDENTIAL_TYPE = "credential_type" -CONF_ROBOT = "robot" -CONF_ROBOT_ID = "robot_id" - -CRED_TYPE_API_KEY = "api-key" -CRED_TYPE_LOCATION_SECRET = "robot-location-secret" diff --git a/homeassistant/components/viam/icons.json b/homeassistant/components/viam/icons.json deleted file mode 100644 index 0145db44d21..00000000000 --- a/homeassistant/components/viam/icons.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "services": { - "capture_image": "mdi:camera", - "capture_data": "mdi:data-matrix", - "get_classifications": "mdi:cctv", - "get_detections": "mdi:cctv" - } -} diff --git a/homeassistant/components/viam/manager.py b/homeassistant/components/viam/manager.py deleted file mode 100644 index 0248ed66197..00000000000 --- a/homeassistant/components/viam/manager.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Manage Viam client connection.""" - -from typing import Any - -from viam.app.app_client import RobotPart -from viam.app.viam_client import ViamClient -from viam.robot.client import RobotClient -from viam.rpc.dial import Credentials, DialOptions - -from homeassistant.const import CONF_ADDRESS -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError - -from .const import ( - CONF_API_ID, - CONF_CREDENTIAL_TYPE, - CONF_ROBOT_ID, - CONF_SECRET, - CRED_TYPE_API_KEY, - CRED_TYPE_LOCATION_SECRET, - DOMAIN, -) - - -class ViamManager: - """Manage Viam client and entry data.""" - - def __init__( - self, - hass: HomeAssistant, - viam: ViamClient, - entry_id: str, - data: dict[str, Any], - ) -> None: - """Store initialized client and user input data.""" - self.address: str = data.get(CONF_ADDRESS, "") - self.auth_entity: str = data.get(CONF_API_ID, "") - self.cred_type: str = data.get(CONF_CREDENTIAL_TYPE, CRED_TYPE_API_KEY) - self.entry_id = entry_id - self.hass = hass - self.robot_id: str = data.get(CONF_ROBOT_ID, "") - self.secret: str = data.get(CONF_SECRET, "") - self.viam = viam - - def unload(self) -> None: - """Clean up any open clients.""" - self.viam.close() - - async def get_robot_client( - self, robot_secret: str | None, robot_address: str | None - ) -> RobotClient: - """Check initialized data to create robot client.""" - address = self.address - payload = self.secret - cred_type = self.cred_type - auth_entity: str | None = self.auth_entity - - if robot_secret is not None: - if robot_address is None: - raise ServiceValidationError( - "The robot address is required for this connection type.", - translation_domain=DOMAIN, - translation_key="robot_credentials_required", - ) - cred_type = CRED_TYPE_LOCATION_SECRET - auth_entity = None - address = robot_address - payload = robot_secret - - if address is None or payload is None: - raise ServiceValidationError( - "The necessary credentials for the RobotClient could not be found.", - translation_domain=DOMAIN, - translation_key="robot_credentials_not_found", - ) - - credentials = Credentials(type=cred_type, payload=payload) - robot_options = RobotClient.Options( - refresh_interval=0, - dial_options=DialOptions(auth_entity=auth_entity, credentials=credentials), - ) - return await RobotClient.at_address(address, robot_options) - - async def get_robot_parts(self) -> list[RobotPart]: - """Retrieve list of robot parts.""" - return await self.viam.app_client.get_robot_parts(robot_id=self.robot_id) diff --git a/homeassistant/components/viam/manifest.json b/homeassistant/components/viam/manifest.json deleted file mode 100644 index 6626d2e3ddf..00000000000 --- a/homeassistant/components/viam/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "viam", - "name": "Viam", - "codeowners": ["@hipsterbrown"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/viam", - "integration_type": "hub", - "iot_class": "cloud_polling", - "requirements": ["viam-sdk==0.17.0"] -} diff --git a/homeassistant/components/viam/services.py b/homeassistant/components/viam/services.py deleted file mode 100644 index fbe0169d551..00000000000 --- a/homeassistant/components/viam/services.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Services for Viam integration.""" - -from __future__ import annotations - -import base64 -from datetime import datetime -from functools import partial - -from PIL import Image -from viam.app.app_client import RobotPart -from viam.services.vision import VisionClient -from viam.services.vision.client import RawImage -import voluptuous as vol - -from homeassistant.components import camera -from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.core import ( - HomeAssistant, - ServiceCall, - ServiceResponse, - SupportsResponse, - callback, -) -from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import selector - -from .const import DOMAIN -from .manager import ViamManager - -ATTR_CONFIG_ENTRY = "config_entry" - -DATA_CAPTURE_SERVICE_NAME = "capture_data" -CAPTURE_IMAGE_SERVICE_NAME = "capture_image" -CLASSIFICATION_SERVICE_NAME = "get_classifications" -DETECTIONS_SERVICE_NAME = "get_detections" - -SERVICE_VALUES = "values" -SERVICE_COMPONENT_NAME = "component_name" -SERVICE_COMPONENT_TYPE = "component_type" -SERVICE_FILEPATH = "filepath" -SERVICE_CAMERA = "camera" -SERVICE_CONFIDENCE = "confidence_threshold" -SERVICE_ROBOT_ADDRESS = "robot_address" -SERVICE_ROBOT_SECRET = "robot_secret" -SERVICE_FILE_NAME = "file_name" -SERVICE_CLASSIFIER_NAME = "classifier_name" -SERVICE_COUNT = "count" -SERVICE_DETECTOR_NAME = "detector_name" - -ENTRY_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( - { - "integration": DOMAIN, - } - ), - } -) -DATA_CAPTURE_SERVICE_SCHEMA = ENTRY_SERVICE_SCHEMA.extend( - { - vol.Required(SERVICE_VALUES): vol.All(dict), - vol.Required(SERVICE_COMPONENT_NAME): vol.All(str), - vol.Required(SERVICE_COMPONENT_TYPE, default="sensor"): vol.All(str), - } -) - -IMAGE_SERVICE_FIELDS = ENTRY_SERVICE_SCHEMA.extend( - { - vol.Optional(SERVICE_FILEPATH): vol.All(str, vol.IsFile), - vol.Optional(SERVICE_CAMERA): vol.All(str), - } -) -VISION_SERVICE_FIELDS = IMAGE_SERVICE_FIELDS.extend( - { - vol.Optional(SERVICE_CONFIDENCE, default="0.6"): vol.All( - str, vol.Coerce(float), vol.Range(min=0, max=1) - ), - vol.Optional(SERVICE_ROBOT_ADDRESS): vol.All(str), - vol.Optional(SERVICE_ROBOT_SECRET): vol.All(str), - } -) - -CAPTURE_IMAGE_SERVICE_SCHEMA = IMAGE_SERVICE_FIELDS.extend( - { - vol.Optional(SERVICE_FILE_NAME, default="camera"): vol.All(str), - vol.Optional(SERVICE_COMPONENT_NAME): vol.All(str), - } -) - -CLASSIFICATION_SERVICE_SCHEMA = VISION_SERVICE_FIELDS.extend( - { - vol.Required(SERVICE_CLASSIFIER_NAME): vol.All(str), - vol.Optional(SERVICE_COUNT, default="2"): vol.All(str, vol.Coerce(int)), - } -) - -DETECTIONS_SERVICE_SCHEMA = VISION_SERVICE_FIELDS.extend( - { - vol.Required(SERVICE_DETECTOR_NAME): vol.All(str), - } -) - - -def __fetch_image(filepath: str | None) -> Image.Image | None: - if filepath is None: - return None - return Image.open(filepath) - - -def __encode_image(image: Image.Image | RawImage) -> str: - """Create base64-encoded Image string.""" - if isinstance(image, Image.Image): - image_bytes = image.tobytes() - else: # RawImage - image_bytes = image.data - - image_string = base64.b64encode(image_bytes).decode() - return f"data:image/jpeg;base64,{image_string}" - - -async def __get_image( - hass: HomeAssistant, filepath: str | None, camera_entity: str | None -) -> RawImage | Image.Image | None: - """Retrieve image type from camera entity or file system.""" - if filepath is not None: - return await hass.async_add_executor_job(__fetch_image, filepath) - if camera_entity is not None: - image = await camera.async_get_image(hass, camera_entity) - return RawImage(image.content, image.content_type) - - return None - - -def __get_manager(hass: HomeAssistant, call: ServiceCall) -> ViamManager: - entry_id: str = call.data[ATTR_CONFIG_ENTRY] - entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id) - - if not entry: - raise ServiceValidationError( - f"Invalid config entry: {entry_id}", - translation_domain=DOMAIN, - translation_key="invalid_config_entry", - translation_placeholders={ - "config_entry": entry_id, - }, - ) - if entry.state != ConfigEntryState.LOADED: - raise ServiceValidationError( - f"{entry.title} is not loaded", - translation_domain=DOMAIN, - translation_key="unloaded_config_entry", - translation_placeholders={ - "config_entry": entry.title, - }, - ) - - manager: ViamManager = hass.data[DOMAIN][entry_id] - return manager - - -async def __capture_data(call: ServiceCall, *, hass: HomeAssistant) -> None: - """Accept input from service call to send to Viam.""" - manager: ViamManager = __get_manager(hass, call) - parts: list[RobotPart] = await manager.get_robot_parts() - values = [call.data.get(SERVICE_VALUES, {})] - component_type = call.data.get(SERVICE_COMPONENT_TYPE, "sensor") - component_name = call.data.get(SERVICE_COMPONENT_NAME, "") - - await manager.viam.data_client.tabular_data_capture_upload( - tabular_data=values, - part_id=parts.pop().id, - component_type=component_type, - component_name=component_name, - method_name="capture_data", - data_request_times=[(datetime.now(), datetime.now())], - ) - - -async def __capture_image(call: ServiceCall, *, hass: HomeAssistant) -> None: - """Accept input from service call to send to Viam.""" - manager: ViamManager = __get_manager(hass, call) - parts: list[RobotPart] = await manager.get_robot_parts() - filepath = call.data.get(SERVICE_FILEPATH) - camera_entity = call.data.get(SERVICE_CAMERA) - component_name = call.data.get(SERVICE_COMPONENT_NAME) - file_name = call.data.get(SERVICE_FILE_NAME, "camera") - - if filepath is not None: - await manager.viam.data_client.file_upload_from_path( - filepath=filepath, - part_id=parts.pop().id, - component_name=component_name, - ) - if camera_entity is not None: - image = await camera.async_get_image(hass, camera_entity) - await manager.viam.data_client.file_upload( - part_id=parts.pop().id, - component_name=component_name, - file_name=file_name, - file_extension=".jpeg", - data=image.content, - ) - - -async def __get_service_values( - hass: HomeAssistant, call: ServiceCall, service_config_name: str -): - """Create common values for vision services.""" - manager: ViamManager = __get_manager(hass, call) - filepath = call.data.get(SERVICE_FILEPATH) - camera_entity = call.data.get(SERVICE_CAMERA) - service_name = call.data.get(service_config_name, "") - count = int(call.data.get(SERVICE_COUNT, 2)) - confidence_threshold = float(call.data.get(SERVICE_CONFIDENCE, 0.6)) - - async with await manager.get_robot_client( - call.data.get(SERVICE_ROBOT_SECRET), call.data.get(SERVICE_ROBOT_ADDRESS) - ) as robot: - service: VisionClient = VisionClient.from_robot(robot, service_name) - image = await __get_image(hass, filepath, camera_entity) - - return manager, service, image, filepath, confidence_threshold, count - - -async def __get_classifications( - call: ServiceCall, *, hass: HomeAssistant -) -> ServiceResponse: - """Accept input configuration to request classifications.""" - ( - manager, - classifier, - image, - filepath, - confidence_threshold, - count, - ) = await __get_service_values(hass, call, SERVICE_CLASSIFIER_NAME) - - if image is None: - return { - "classifications": [], - "img_src": filepath or None, - } - - img_src = filepath or __encode_image(image) - classifications = await classifier.get_classifications(image, count) - - return { - "classifications": [ - {"name": c.class_name, "confidence": c.confidence} - for c in classifications - if c.confidence >= confidence_threshold - ], - "img_src": img_src, - } - - -async def __get_detections( - call: ServiceCall, *, hass: HomeAssistant -) -> ServiceResponse: - """Accept input configuration to request detections.""" - ( - manager, - detector, - image, - filepath, - confidence_threshold, - _count, - ) = await __get_service_values(hass, call, SERVICE_DETECTOR_NAME) - - if image is None: - return { - "detections": [], - "img_src": filepath or None, - } - - img_src = filepath or __encode_image(image) - detections = await detector.get_detections(image) - - return { - "detections": [ - { - "name": c.class_name, - "confidence": c.confidence, - "x_min": c.x_min, - "y_min": c.y_min, - "x_max": c.x_max, - "y_max": c.y_max, - } - for c in detections - if c.confidence >= confidence_threshold - ], - "img_src": img_src, - } - - -@callback -def async_setup_services(hass: HomeAssistant) -> None: - """Set up services for Viam integration.""" - - hass.services.async_register( - DOMAIN, - DATA_CAPTURE_SERVICE_NAME, - partial(__capture_data, hass=hass), - DATA_CAPTURE_SERVICE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - CAPTURE_IMAGE_SERVICE_NAME, - partial(__capture_image, hass=hass), - CAPTURE_IMAGE_SERVICE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - CLASSIFICATION_SERVICE_NAME, - partial(__get_classifications, hass=hass), - CLASSIFICATION_SERVICE_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) - hass.services.async_register( - DOMAIN, - DETECTIONS_SERVICE_NAME, - partial(__get_detections, hass=hass), - DETECTIONS_SERVICE_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) diff --git a/homeassistant/components/viam/services.yaml b/homeassistant/components/viam/services.yaml deleted file mode 100644 index 76a35e1ff06..00000000000 --- a/homeassistant/components/viam/services.yaml +++ /dev/null @@ -1,98 +0,0 @@ -capture_data: - fields: - values: - required: true - selector: - object: - component_name: - required: true - selector: - text: - component_type: - required: false - selector: - text: -capture_image: - fields: - filepath: - required: false - selector: - text: - camera: - required: false - selector: - entity: - filter: - domain: camera - file_name: - required: false - selector: - text: - component_name: - required: false - selector: - text: -get_classifications: - fields: - classifier_name: - required: true - selector: - text: - confidence: - required: false - default: 0.6 - selector: - text: - type: number - count: - required: false - selector: - number: - robot_address: - required: false - selector: - text: - robot_secret: - required: false - selector: - text: - filepath: - required: false - selector: - text: - camera: - required: false - selector: - entity: - filter: - domain: camera -get_detections: - fields: - detector_name: - required: true - selector: - text: - confidence: - required: false - default: 0.6 - selector: - text: - type: number - robot_address: - required: false - selector: - text: - robot_secret: - required: false - selector: - text: - filepath: - required: false - selector: - text: - camera: - required: false - selector: - entity: - filter: - domain: camera diff --git a/homeassistant/components/viam/strings.json b/homeassistant/components/viam/strings.json deleted file mode 100644 index e6074749ca7..00000000000 --- a/homeassistant/components/viam/strings.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Authenticate with Viam", - "description": "Select which credential type to use.", - "data": { - "credential_type": "Credential type" - } - }, - "auth": { - "title": "[%key:component::viam::config::step::user::title%]", - "description": "Provide the credentials for communicating with the Viam service.", - "data": { - "api_id": "API key ID", - "api_key": "API key", - "address": "Robot address", - "secret": "Robot secret" - }, - "data_description": { - "address": "Find this under the Code Sample tab in the app.", - "secret": "Find this under the Code Sample tab in the app when 'include secret' is enabled." - } - }, - "robot": { - "data": { - "robot": "Select a robot" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - } - }, - "selector": { - "credential_type": { - "options": { - "api-key": "Org API key", - "robot-location-secret": "Robot location secret" - } - } - }, - "exceptions": { - "entry_not_found": { - "message": "No Viam config entries found" - }, - "entry_not_loaded": { - "message": "{config_entry_title} is not loaded" - }, - "invalid_config_entry": { - "message": "Invalid config entry provided. Got {config_entry}" - }, - "unloaded_config_entry": { - "message": "Invalid config entry provided. {config_entry} is not loaded." - }, - "robot_credentials_required": { - "message": "The robot address is required for this connection type." - }, - "robot_credentials_not_found": { - "message": "The necessary credentials for the RobotClient could not be found." - } - }, - "services": { - "capture_data": { - "name": "Capture data", - "description": "Send arbitrary tabular data to Viam for analytics and model training.", - "fields": { - "values": { - "name": "Values", - "description": "List of tabular data to send to Viam." - }, - "component_name": { - "name": "Component name", - "description": "The name of the configured robot component to use." - }, - "component_type": { - "name": "Component type", - "description": "The type of the associated component." - } - } - }, - "capture_image": { - "name": "Capture image", - "description": "Send images to Viam for analytics and model training.", - "fields": { - "filepath": { - "name": "Filepath", - "description": "Local file path to the image you wish to reference." - }, - "camera": { - "name": "Camera entity", - "description": "The camera entity from which an image is captured." - }, - "file_name": { - "name": "File name", - "description": "The name of the file that will be displayed in the metadata within Viam." - }, - "component_name": { - "name": "Component name", - "description": "The name of the configured robot component to use." - } - } - }, - "get_classifications": { - "name": "Classify images", - "description": "Get a list of classifications from an image.", - "fields": { - "classifier_name": { - "name": "Classifier name", - "description": "Name of classifier vision service configured in Viam" - }, - "confidence": { - "name": "Confidence", - "description": "Threshold for filtering results returned by the service" - }, - "count": { - "name": "Classification count", - "description": "Number of classifications to return from the service" - }, - "robot_address": { - "name": "Robot address", - "description": "If authenticated using an Org API key, provide the robot address associated with the configured vision service." - }, - "robot_secret": { - "name": "Robot secret", - "description": "If authenticated using an Org API key, provide the robot location secret associated with the configured vision service." - }, - "filepath": { - "name": "Filepath", - "description": "Local file path to the image you wish to reference." - }, - "camera": { - "name": "Camera entity", - "description": "The camera entity from which an image is captured." - } - } - }, - "get_detections": { - "name": "Detect objects in images", - "description": "Get a list of detected objects from an image.", - "fields": { - "detector_name": { - "name": "Detector name", - "description": "Name of detector vision service configured in Viam" - }, - "confidence": { - "name": "Confidence", - "description": "Threshold for filtering results returned by the service" - }, - "robot_address": { - "name": "Robot address", - "description": "If authenticated using an Org API key, provide the robot address associated with the configured vision service." - }, - "robot_secret": { - "name": "Robot secret", - "description": "If authenticated using an Org API key, provide the robot location secret associated with the configured vision service." - }, - "filepath": { - "name": "Filepath", - "description": "Local file path to the image you wish to reference." - }, - "camera": { - "name": "Camera entity", - "description": "The camera entity from which an image is captured." - } - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1987581ff7c..9f24c9676e5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -592,7 +592,6 @@ FLOWS = { "verisure", "version", "vesync", - "viam", "vicare", "vilfo", "vizio", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 7c2f8a95de5..d5199e6ba1e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6603,12 +6603,6 @@ "config_flow": false, "iot_class": "cloud_polling" }, - "viam": { - "name": "Viam", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "vicare": { "name": "Viessmann ViCare", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 0a1c8a9899e..bc1457cd374 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2816,9 +2816,6 @@ velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 -# homeassistant.components.viam -viam-sdk==0.17.0 - # homeassistant.components.vilfo vilfo-api-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcb3484f30f..06e7ae8fd30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2184,9 +2184,6 @@ velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 -# homeassistant.components.viam -viam-sdk==0.17.0 - # homeassistant.components.vilfo vilfo-api-client==0.5.0 diff --git a/tests/components/viam/__init__.py b/tests/components/viam/__init__.py deleted file mode 100644 index f606728242e..00000000000 --- a/tests/components/viam/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the viam integration.""" diff --git a/tests/components/viam/conftest.py b/tests/components/viam/conftest.py deleted file mode 100644 index 3da6b272145..00000000000 --- a/tests/components/viam/conftest.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Common fixtures for the viam tests.""" - -import asyncio -from collections.abc import Generator -from dataclasses import dataclass -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from viam.app.viam_client import ViamClient - - -@dataclass -class MockLocation: - """Fake location for testing.""" - - id: str = "13" - name: str = "home" - - -@dataclass -class MockRobot: - """Fake robot for testing.""" - - id: str = "1234" - name: str = "test" - - -def async_return(result): - """Allow async return value with MagicMock.""" - - future = asyncio.Future() - future.set_result(result) - return future - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.viam.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture(name="mock_viam_client") -def mock_viam_client_fixture() -> Generator[tuple[MagicMock, MockRobot], None, None]: - """Override ViamClient from Viam SDK.""" - with ( - patch("viam.app.viam_client.ViamClient") as MockClient, - patch.object(ViamClient, "create_from_dial_options") as mock_create_client, - ): - instance: MagicMock = MockClient.return_value - mock_create_client.return_value = instance - - mock_location = MockLocation() - mock_robot = MockRobot() - instance.app_client.list_locations.return_value = async_return([mock_location]) - instance.app_client.get_location.return_value = async_return(mock_location) - instance.app_client.list_robots.return_value = async_return([mock_robot]) - yield instance, mock_robot diff --git a/tests/components/viam/test_config_flow.py b/tests/components/viam/test_config_flow.py deleted file mode 100644 index 8ab6edb154f..00000000000 --- a/tests/components/viam/test_config_flow.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Test the viam config flow.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from viam.app.viam_client import ViamClient - -from homeassistant import config_entries -from homeassistant.components.viam.config_flow import CannotConnect -from homeassistant.components.viam.const import ( - CONF_API_ID, - CONF_CREDENTIAL_TYPE, - CONF_ROBOT, - CONF_ROBOT_ID, - CONF_SECRET, - CRED_TYPE_API_KEY, - CRED_TYPE_LOCATION_SECRET, - DOMAIN, -) -from homeassistant.const import CONF_ADDRESS, CONF_API_KEY -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from .conftest import MockRobot - -pytestmark = pytest.mark.usefixtures("mock_setup_entry") - - -async def test_user_form( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_viam_client: Generator[tuple[MagicMock, MockRobot], None, None], -) -> None: - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_CREDENTIAL_TYPE: CRED_TYPE_API_KEY, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - assert result["errors"] == {} - - _client, mock_robot = mock_viam_client - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_ID: "someTestId", - CONF_API_KEY: "randomSecureAPIKey", - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] is None - assert result["step_id"] == "robot" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ROBOT: mock_robot.id, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "home" - assert result["data"] == { - CONF_API_ID: "someTestId", - CONF_API_KEY: "randomSecureAPIKey", - CONF_ROBOT_ID: mock_robot.id, - CONF_CREDENTIAL_TYPE: CRED_TYPE_API_KEY, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_user_form_with_location_secret( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_viam_client: Generator[tuple[MagicMock, MockRobot], None, None], -) -> None: - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_CREDENTIAL_TYPE: CRED_TYPE_LOCATION_SECRET, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_robot_location" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: "my.robot.cloud", - CONF_SECRET: "randomSecreteForRobot", - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] is None - assert result["step_id"] == "robot" - - _client, mock_robot = mock_viam_client - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ROBOT: mock_robot.id, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "home" - assert result["data"] == { - CONF_ADDRESS: "my.robot.cloud", - CONF_SECRET: "randomSecreteForRobot", - CONF_ROBOT_ID: mock_robot.id, - CONF_CREDENTIAL_TYPE: CRED_TYPE_LOCATION_SECRET, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -@patch( - "viam.app.viam_client.ViamClient.create_from_dial_options", - side_effect=CannotConnect, -) -async def test_form_missing_secret( - _mock_create_client: AsyncMock, hass: HomeAssistant -) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_CREDENTIAL_TYPE: CRED_TYPE_API_KEY, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_ID: "someTestId", - CONF_API_KEY: "", - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - assert result["errors"] == {"base": "cannot_connect"} - - -@patch.object(ViamClient, "create_from_dial_options", return_value=None) -async def test_form_cannot_connect( - _mock_create_client: AsyncMock, - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_CREDENTIAL_TYPE: CRED_TYPE_API_KEY, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_ID: "someTestId", - CONF_API_KEY: "randomSecureAPIKey", - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - assert result["errors"] == {"base": "cannot_connect"} - - -@patch( - "viam.app.viam_client.ViamClient.create_from_dial_options", side_effect=Exception -) -async def test_form_exception( - _mock_create_client: AsyncMock, hass: HomeAssistant -) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_CREDENTIAL_TYPE: CRED_TYPE_API_KEY, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_ID: "someTestId", - CONF_API_KEY: "randomSecureAPIKey", - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "auth_api_key" - assert result["errors"] == {"base": "unknown"}