Skip to content

Commit

Permalink
Support for networked projectors
Browse files Browse the repository at this point in the history
  • Loading branch information
rrooggiieerr committed Dec 20, 2023
1 parent 5d3736b commit 38a426a
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 37 deletions.
108 changes: 77 additions & 31 deletions custom_components/benqprojector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
import homeassistant.helpers.config_validation as cv
import serial
import voluptuous as vol
from benqprojector import BenQProjector
from benqprojector import BenQProjector, BenQProjectorSerial, BenQProjectorTelnet
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, Platform
from homeassistant.const import CONF_DEVICE_ID, Platform, CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_BAUD_RATE, CONF_PROJECTOR, CONF_SERIAL_PORT, DOMAIN
from .const import CONF_BAUD_RATE, CONF_PROJECTOR, CONF_SERIAL_PORT, CONF_TYPE_SERIAL, CONF_TYPE_TELNET, DOMAIN

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -59,7 +59,7 @@ class BenQProjectorCoordinator(DataUpdateCoordinator):

_listener_commands = []

def __init__(self, hass, serial_port: str, baud_rate: int):
def __init__(self, hass, projector: BenQProjector):
"""Initialize BenQ Projector Data Update Coordinator."""
super().__init__(
hass,
Expand All @@ -70,21 +70,7 @@ def __init__(self, hass, serial_port: str, baud_rate: int):
update_interval=timedelta(seconds=5),
)

self._serial_port = serial_port
self.projector = BenQProjector(self._serial_port, baud_rate)

async def async_connect(self):
try:
if not await self.hass.async_add_executor_job(self.projector.connect):
raise ConfigEntryNotReady(
f"Unable to connect to BenQ projector on {self._serial_port}"
)
except TimeoutError as ex:
raise ConfigEntryNotReady(
f"Unable to connect to BenQ projector on {self._serial_port}", ex
)

_LOGGER.debug("Connected to BenQ projector on %s", self._serial_port)
self.projector = projector

self.unique_id = self.projector.unique_id
self.model = self.projector.model
Expand All @@ -97,6 +83,30 @@ async def async_connect(self):
manufacturer="BenQ",
)

# async def async_connect(self):
# try:
# if not await self.hass.async_add_executor_job(self.projector.connect):
# raise ConfigEntryNotReady(
# f"Unable to connect to BenQ projector on {self._serial_port}"
# )
# except TimeoutError as ex:
# raise ConfigEntryNotReady(
# f"Unable to connect to BenQ projector on {self._serial_port}", ex
# )
#
# _LOGGER.debug("Connected to BenQ projector on %s", self._serial_port)
#
# self.unique_id = self.projector.unique_id
# self.model = self.projector.model
# self.power_status = self.projector.power_status
#
# self.device_info = DeviceInfo(
# identifiers={(DOMAIN, self.unique_id)},
# name=f"BenQ {self.model}",
# model=self.model,
# manufacturer="BenQ",
# )

async def async_disconnect(self):
await self.hass.async_add_executor_job(self.projector.disconnect)
_LOGGER.debug("Disconnected from BenQ projector on %s", self._serial_port)
Expand Down Expand Up @@ -219,7 +229,7 @@ async def _async_update_data(self):
if (ltim2 := await self.async_send_command("ltim2")) is not None:
data["ltim2"] = ltim2

if self.power_status == BenQProjector.POWERSTATUS_ON:
if self.power_status == self.projector.POWERSTATUS_ON:
await self.hass.async_add_executor_job(self.projector.update_volume)
volume_level = None
if self.projector.volume is not None:
Expand All @@ -241,20 +251,56 @@ async def _async_update_data(self):

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BenQ Projector from a config entry."""
try:
serial_port = entry.data[CONF_SERIAL_PORT]
coordinator = BenQProjectorCoordinator(
hass, serial_port, entry.data[CONF_BAUD_RATE]
)
projector = None

conf_type = CONF_TYPE_SERIAL
if CONF_TYPE in entry.data:
conf_type = entry.data[CONF_TYPE]

if conf_type == CONF_TYPE_TELNET:
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]

# Test if we can connect to the device.
projector = BenQProjectorTelnet(host, port)

# Open the connection.
await coordinator.async_connect()
if not await hass.async_add_executor_job(projector.connect):
raise ConfigEntryNotReady(f"Unable to connect to device {host}:{port}")
else:
serial_port = entry.data[CONF_SERIAL_PORT]
baud_rate = entry.data[CONF_BAUD_RATE]

_LOGGER.info("BenQ projector on %s is available", serial_port)
except serial.SerialException as ex:
raise ConfigEntryNotReady(
f"Unable to connect to BenQ projector on {serial_port}: {ex}"
) from ex
# Test if we can connect to the device.
try:
projector = BenQProjectorSerial(serial_port, baud_rate)

# Open the connection.
if not await hass.async_add_executor_job(projector.connect):
raise ConfigEntryNotReady(f"Unable to connect to device {serial_port}")

_LOGGER.info("Device %s is available", serial_port)
except serial.SerialException as ex:
raise ConfigEntryNotReady(
f"Unable to connect to device {serial_port}"
) from ex

coordinator = BenQProjectorCoordinator(hass, projector)

# try:
# serial_port = entry.data[CONF_SERIAL_PORT]
# coordinator = BenQProjectorCoordinator(
# hass, serial_port, entry.data[CONF_BAUD_RATE]
# )
#
# # Open the connection.
# await coordinator.async_connect()
#
# _LOGGER.info("BenQ projector on %s is available", serial_port)
# except serial.SerialException as ex:
# raise ConfigEntryNotReady(
# f"Unable to connect to BenQ projector on {serial_port}: {ex}"
# ) from ex

# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
Expand Down
98 changes: 93 additions & 5 deletions custom_components/benqprojector/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@

import logging
import os
from typing import Any
from typing import Any, Final

import homeassistant.helpers.config_validation as cv
import serial
import serial.tools.list_ports
import voluptuous as vol
from benqprojector import BAUD_RATES, BenQProjector
from benqprojector import BAUD_RATES, BenQProjectorSerial, BenQProjectorTelnet
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError

from .const import CONF_BAUD_RATE, CONF_MANUAL_PATH, CONF_SERIAL_PORT, DOMAIN
from .const import CONF_BAUD_RATE, CONF_MANUAL_PATH, CONF_SERIAL_PORT, CONF_TYPE_SERIAL, CONF_TYPE_TELNET, DOMAIN

_LOGGER = logging.getLogger(__name__)

DEFAULT_PORT: Final = 8000

class BenQProjectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BenQ Projector."""
Expand All @@ -26,11 +28,28 @@ class BenQProjectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

STEP_SETUP_SERIAL_SCHEMA = None

_step_setup_network_schema = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
return await self.async_step_setup_serial(user_input)
if user_input is not None:
user_selection = user_input[CONF_TYPE]
if user_selection == "Serial":
return await self.async_step_setup_serial()

return await self.async_step_setup_network()

list_of_types = ["Serial", "Network"]

schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)})
return self.async_show_form(step_id="user", data_schema=schema)

async def async_step_setup_serial(
self, user_input: dict[str, Any] | None = None
Expand Down Expand Up @@ -111,7 +130,7 @@ async def validate_input_setup_serial(
# Test if we can connect to the device
try:
# Get model from the device
projector = BenQProjector(serial_port, data[CONF_BAUD_RATE])
projector = BenQProjectorSerial(serial_port, data[CONF_BAUD_RATE])
# projector.connect()
if not self.hass.async_add_executor_job(projector.connect):
raise CannotConnect(f"Unable to connect to the device {serial_port}")
Expand All @@ -134,6 +153,75 @@ async def validate_input_setup_serial(
None,
)

async def async_step_setup_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Step when setting up network configuration."""
errors: dict[str, str] = {}

if user_input is not None:
try:
info = await self.validate_input_setup_network(user_input, errors)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", ex)
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=info)
# data = await self.validate_input_setup_network(user_input, errors)
# if not errors:
# return self.async_create_entry(
# title=f"{data[CONF_HOST]}:{data[CONF_PORT]}", data=data
# )

return self.async_show_form(
step_id="setup_network",
data_schema=self._step_setup_network_schema,
errors=errors,
)

async def validate_input_setup_network(
self, data: dict[str, Any], errors: dict[str, str]
) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from _step_setup_network_schema with values provided by the user.
"""
# Validate the data can be used to set up a network connection.
self._step_setup_network_schema(data)

host = data[CONF_HOST]
port = data[CONF_PORT]

#ToDo Test if the host exists

await self.async_set_unique_id(f"{host}:{port}")
self._abort_if_unique_id_configured()

# Test if we can connect to the device.
try:
projector = BenQProjectorTelnet(host, port)
if not await self.hass.async_add_executor_job(projector.connect):
raise CannotConnect(f"Unable to connect to the device on {host}:{port}")

model = projector.model

await self.hass.async_add_executor_job(projector.disconnect)
_LOGGER.info("Device on %s:%s available", host, port)
except serial.SerialException as ex:
raise CannotConnect(
f"Unable to connect to the device on {host}:{port}"
) from ex

# Return info that you want to store in the config entry.
return {
"title": f"BenQ {model} {host}:{port}",
CONF_TYPE: CONF_TYPE_TELNET,
CONF_HOST: host,
CONF_PORT: port,
}


def get_serial_by_id(dev_path: str) -> str:
"""Return a /dev/serial/by-id match for given device if available."""
Expand Down
3 changes: 3 additions & 0 deletions custom_components/benqprojector/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

DOMAIN: Final = "benqprojector"

CONF_TYPE_SERIAL: Final = "serial"
CONF_TYPE_TELNET: Final = "telnet"

CONF_SERIAL_PORT: Final = "serial_port"
CONF_MANUAL_PATH: Final = "manual_path"
CONF_PROJECTOR: Final = "projector"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/benqprojector/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"benqprojector"
],
"requirements": [
"benqprojector==0.0.12.4"
"benqprojector==0.0.13.1"
],
"version": "0.0.9"
}

0 comments on commit 38a426a

Please sign in to comment.