Skip to content
Open
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
9 changes: 9 additions & 0 deletions homeassistant/components/sony_projector/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Agent Instructions for `sony_projector`

- Always run network operations for this integration via `async_add_executor_job` to avoid blocking the event loop.
- Use the projector host as the config entry unique ID and prevent duplicate flows based on the host.
- Do not ask users to provide a custom name during configuration; rely on the built-in default title instead.
- When writing tests, patch `homeassistant.components.sony_projector.config_flow.pysdcp.Projector` to avoid real I/O.
- Address lint feedback directly instead of suppressing it; avoid adding unnecessary `noqa` directives.
- Enable the config entry flow only when the integration implements `async_setup_entry`/`async_unload_entry` and forwards to the switch platform so UI-configured users get entities immediately.
- The development environment pins Ruff <0.13, so `ruff format` is unavailable—prefer `ruff check --fix` and manual formatting updates when adjusting code here.
43 changes: 43 additions & 0 deletions homeassistant/components/sony_projector/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,44 @@
"""The sony_projector component."""

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType

DOMAIN = "sony_projector"
PLATFORMS = [Platform.SWITCH]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Sony Projector integration."""

hass.data.setdefault(DOMAIN, {})
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Sony Projector from a config entry."""

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = dict(entry.data)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a Sony Projector config entry."""

unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id, None)

return unload_ok


async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry formats to the latest version."""

return True
127 changes: 127 additions & 0 deletions homeassistant/components/sony_projector/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Config flow for the Sony Projector integration."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

import pysdcp
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

DOMAIN = "sony_projector"
DEFAULT_TITLE = "Sony Projector"

STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})


async def _async_validate_input(hass: HomeAssistant, data: Mapping[str, Any]) -> None:
"""Validate the host information provided by the user."""

projector = pysdcp.Projector(data[CONF_HOST])

try:
await hass.async_add_executor_job(projector.get_power)
except ConnectionError as err:
raise CannotConnect from err
except OSError as err:
raise CannotConnect from err
except Exception as err:
raise UnknownError from err


class SonyProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Sony Projector."""

VERSION = 1

async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""

errors: dict[str, str] = {}

if user_input is not None:
host = user_input[CONF_HOST]

try:
await _async_validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except UnknownError:
errors["base"] = "unknown"
else:
await self.async_set_unique_id(host)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})

return self.async_create_entry(
title=DEFAULT_TITLE,
data={CONF_HOST: host},
)
Comment on lines +42 to +65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Config entries created but integration never sets up entities

The flow now calls async_create_entry and the manifest enables config_flow, but the integration still only exposes the legacy setup_platform in switch.py and has no async_setup_entry to load platforms from a config entry. After users complete the UI flow, Home Assistant will store the entry but never create a switch, leaving the projector unusable. Config entry support (e.g. forwarding to the switch platform) needs to be implemented in the integration before exposing a flow.

Useful? React with 👍 / 👎.


return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

async def async_step_import(self, import_data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle import from YAML."""

host = import_data[CONF_HOST]

await _async_validate_input(self.hass, import_data)

await self.async_set_unique_id(host)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._async_abort_entries_match({CONF_HOST: host})

data = {CONF_HOST: host}
if CONF_NAME in import_data:
data[CONF_NAME] = import_data[CONF_NAME]

return self.async_create_entry(
title=DEFAULT_TITLE,
data=data,
)

async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle reauthentication flow."""

return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication by testing connectivity."""

errors: dict[str, str] = {}
entry = self._get_reauth_entry()
host = entry.data[CONF_HOST]

if user_input is not None:
try:
await _async_validate_input(self.hass, entry.data)
except CannotConnect:
errors["base"] = "cannot_connect"
except UnknownError:
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(entry, data_updates=entry.data)

return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"host": host},
errors=errors,
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class UnknownError(HomeAssistantError):
"""Error to indicate an unknown error occurred."""
1 change: 1 addition & 0 deletions homeassistant/components/sony_projector/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"domain": "sony_projector",
"name": "Sony Projector",
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sony_projector",
"iot_class": "local_polling",
"loggers": ["pysdcp"],
Expand Down
25 changes: 25 additions & 0 deletions homeassistant/components/sony_projector/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Reconnect to the projector at {host}."
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}
Loading
Loading