From d6c34f6e0a303ab971920f379d81d251c80c9caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Dahl?= <80645006+Sdahl1234@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:48:33 +0100 Subject: [PATCH] Add files via upload --- .../seven_segments_alt/__init__.py | 222 ++++++++++++++++++ .../seven_segments_alt/button.py | 55 +++++ .../seven_segments_alt/config_flow.py | 137 +++++++++++ custom_components/seven_segments_alt/const.py | 13 + .../seven_segments_alt/entity.py | 23 ++ custom_components/seven_segments_alt/image.py | 153 ++++++++++++ .../seven_segments_alt/image_processing.py | 195 +++++++++++++++ .../seven_segments_alt/manifest.json | 11 + .../seven_segments_alt/number.py | 60 +++++ .../seven_segments_alt/sensor.py | 53 +++++ custom_components/seven_segments_alt/text.py | 52 ++++ .../seven_segments_alt/translations/en.json | 77 ++++++ hacs.json | 0 13 files changed, 1051 insertions(+) create mode 100644 custom_components/seven_segments_alt/__init__.py create mode 100644 custom_components/seven_segments_alt/button.py create mode 100644 custom_components/seven_segments_alt/config_flow.py create mode 100644 custom_components/seven_segments_alt/const.py create mode 100644 custom_components/seven_segments_alt/entity.py create mode 100644 custom_components/seven_segments_alt/image.py create mode 100644 custom_components/seven_segments_alt/image_processing.py create mode 100644 custom_components/seven_segments_alt/manifest.json create mode 100644 custom_components/seven_segments_alt/number.py create mode 100644 custom_components/seven_segments_alt/sensor.py create mode 100644 custom_components/seven_segments_alt/text.py create mode 100644 custom_components/seven_segments_alt/translations/en.json create mode 100644 hacs.json diff --git a/custom_components/seven_segments_alt/__init__.py b/custom_components/seven_segments_alt/__init__.py new file mode 100644 index 0000000..89457e2 --- /dev/null +++ b/custom_components/seven_segments_alt/__init__.py @@ -0,0 +1,222 @@ +"""The seven_segments component.""" +# import asyncio +from datetime import datetime, timedelta +import json +import logging +import os + +from PIL import Image + +from homeassistant.components.image import ImageEntity +from homeassistant.components.image_processing import ImageProcessingEntity +from homeassistant.config import ConfigType +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo + +# from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +# from . import ImageProcessingSsocr +from .const import ( + DATA_COORDINATOR, + DOMAIN, + SS_CAM, + SS_DIGITS, + SS_EXTRA_ARGUMENTS, + SS_HEIGHT, + SS_ROTATE, + SS_THRESHOLD, + SS_WIDTH, + SS_X_POS, + SS_Y_POS, +) + +# from .image_processing import ImageProcessingSsocr + +PLATFORMS = [ + Platform.BUTTON, + Platform.IMAGE, + Platform.NUMBER, + # Platform.SELECT, + Platform.SENSOR, + Platform.TEXT, +] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType): # noqa: D103 + hass.async_create_task( + hass.helpers.discovery.async_load_platform( + Platform.IMAGE_PROCESSING, DOMAIN, {}, config + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the SS.""" + + ei = entry.data.get(CONF_ENTITY_ID) + name = entry.data.get(CONF_NAME) + data_coordinator = SSDataCoordinator(hass, ei, name) + await data_coordinator.file_exits() + await data_coordinator.load_data() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + hass.data[DOMAIN][entry.entry_id] = data_coordinator + hass.data[DOMAIN][DATA_COORDINATOR] = data_coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(async_update_entry)) + return True + + +async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class SSDataCoordinator(DataUpdateCoordinator): # noqa: D101 + # config_entry: ConfigEntry + + # cfile: io.TextIOWrapper = None + componentname: str + img_path: str + processed_name: str + new_image: bool = False + image_entity: ImageEntity + image_entity_2: ImageEntity + image_entity_3: ImageEntity + ocr_image: Image + ocr_entity: ImageProcessingEntity # ImageProcessingSsocr + ocr_state: str = None + camera_entity_id: str + jdata: None + data_loaded: bool = False + data_default = { + SS_X_POS: 0, + SS_Y_POS: 0, + SS_HEIGHT: 0, + SS_WIDTH: 0, + SS_ROTATE: 0, + SS_THRESHOLD: 0, + SS_DIGITS: -1, + SS_EXTRA_ARGUMENTS: "", + SS_CAM: "", + } + + def __init__(self, hass: HomeAssistant, ei: str, name: str) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name=DOMAIN, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=10), # 60 * 60), + ) + self.camera_entity_id = ei + self.always_update = True + self._name = name + self.componentname = name + self.filepath = os.path.join( + self.hass.config.config_dir, + "ssocr-{}.json".format(self.componentname.replace(" ", "_")), + ) + pn = f"{self.componentname}_img_processed.png".replace(" ", "_") + self.processed_name = os.path.join(self.hass.config.config_dir, pn) + # self.processed_name = f"{self.componentname}_img_processed.png" + self.mandatory_extras = f"-D{self.processed_name}" + _LOGGER.debug(self.filepath) + _LOGGER.debug(self.processed_name) + _LOGGER.debug(self.mandatory_extras) + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self.unique_id), + }, + manufacturer="Seven Segments", + name=self.componentname, + ) + + @property + def unique_id(self) -> str: + """Return the system descriptor.""" + return f"{DOMAIN}-{self.componentname}" + + async def set_default_data(self): + """Set default.""" + self.jdata = self.data_default + self.jdata[SS_X_POS] = 990 + self.jdata[SS_Y_POS] = 180 + self.jdata[SS_HEIGHT] = 180 + self.jdata[SS_WIDTH] = 770 + self.jdata[SS_ROTATE] = 0 + self.jdata[SS_THRESHOLD] = 44 + self.jdata[SS_DIGITS] = 7 + self.jdata[SS_EXTRA_ARGUMENTS] = "-cdecimal" + self.jdata[SS_CAM] = self.camera_entity_id + + async def file_exits(self): + """Do file exists.""" + try: + f = open(self.filepath, encoding="utf-8") + f.close() + except FileNotFoundError: + # save a new file + await self.set_default_data() + await self.save_data(False) + + async def save_data(self, append: bool): + """Save data.""" + if append: + cfile = open(self.filepath, "w", encoding="utf-8") + else: + cfile = open(self.filepath, "a", encoding="utf-8") + ocrdata = json.dumps(self.jdata) + cfile.write(ocrdata) + cfile.close() + + async def load_data(self): + """Load data.""" + cfile = open(self.filepath, encoding="utf-8") + ocrdata = cfile.read() + cfile.close() + _LOGGER.debug(f"ocrdata: {ocrdata}") # noqa: G004 + _LOGGER.debug(f"jsonload: {json.loads(ocrdata)}") # noqa: G004 + + self.jdata = json.loads(ocrdata) + self.data_loaded = True + + async def _async_update_data(self): + try: + # await self.hass.async_add_executor_job(self.data_handler.update) + await self.ocr_entity.async_update() + if self.new_image: + self.new_image = False + self.image_entity.image_last_updated = datetime.now() + self.image_entity_2.image_last_updated = datetime.now() + self.image_entity_3.image_last_updated = datetime.now() + return None + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug(f"update failed: {ex}") # noqa: G004 diff --git a/custom_components/seven_segments_alt/button.py b/custom_components/seven_segments_alt/button.py new file mode 100644 index 0000000..f65087b --- /dev/null +++ b/custom_components/seven_segments_alt/button.py @@ -0,0 +1,55 @@ +"""Support for SS.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.button import ButtonEntity +from homeassistant.core import HomeAssistant + +from . import SSDataCoordinator +from .const import DOMAIN +from .entity import SSEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: + """Do setup entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SSButton(coordinator, "Save data", "ss_save_data")]) + async_add_entities([SSButton(coordinator, "Load data", "ss_load_data")]) + + +class SSButton(SSEntity, ButtonEntity): + """SS buttons.""" + + data_coordinator: SSDataCoordinator + + def __init__( + self, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + super().__init__(coordinator) + self.data_coordinator = coordinator + self._attr_translation_key = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self._attr_has_entity_name = True + self.mode = "text" + self._tk = translationkey + self.jname = name + # self._attr_unique_id = ( + # f"{DOMAIN}_{self.data_coordinator.componentname}_{self._tk}" + # ) + + async def async_press(self) -> None: + """Handle the button press.""" + if self._tk == "ss_save_data": + await self.data_coordinator.save_data(True) + await self.data_coordinator.ocr_entity.set_command() + elif self._tk == "ss_load_data": + await self.data_coordinator.load_data() + await self.data_coordinator.ocr_entity.set_command() diff --git a/custom_components/seven_segments_alt/config_flow.py b/custom_components/seven_segments_alt/config_flow.py new file mode 100644 index 0000000..f8ffeaa --- /dev/null +++ b/custom_components/seven_segments_alt/config_flow.py @@ -0,0 +1,137 @@ +"""Adds config flow for ss integration.""" +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er, selector +from homeassistant.helpers.entity_registry import EntityRegistry, EntityRegistryItems + +# from homeassistant.helpers.selector import SelectSelectorMode +from .const import DOMAIN + +# _LOGGER = logging.getLogger(__name__) + +# DATA_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): str, vol.Required(CONF_NAME): str}) + + +async def validate_input(hass: core.HomeAssistant, entity_id, name): + """Validate the user input allows us to connect.""" + + # Pre-validation for missing mandatory fields + if not entity_id: + raise MissingentityidValue("The 'entity_id' field is required.") + if not name: + raise MissingNameValue("The 'name' field is required.") + + for entry in hass.config_entries.async_entries(DOMAIN): + if any( + [ + entry.data[CONF_ENTITY_ID] == entity_id, + entry.data[CONF_NAME] == name, + ] + ): + raise AlreadyConfigured("An entry with the given details already exists.") + + # Additional validations (if any) go here... + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SS integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + cameras = [] + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + entity_id = user_input[CONF_ENTITY_ID] + name = user_input[CONF_NAME] + await validate_input(self.hass, entity_id, name) + + unique_id = f"{DOMAIN}-{name}" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=unique_id, + data={ + CONF_ENTITY_ID: entity_id, + CONF_NAME: name, + }, + ) + + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except MissingentityidValue: + errors["base"] = "missing_entity_id" + except MissingNameValue: + errors["base"] = "missing_name" + + entity_reg: EntityRegistry = er.async_get(self.hass) + Items: EntityRegistryItems = entity_reg.entities + for item in Items.values(): + if item.domain == "camera" and item.disabled is False: + self.cameras.append(item.entity_id) + + data = { + vol.Required(CONF_NAME): str, + vol.Required(CONF_ENTITY_ID, default=False): selector.SelectSelector( + selector.SelectSelectorConfig( + options=self.cameras, mode=selector.SelectSelectorMode.DROPDOWN + ), + ), + } + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(data), + errors=errors, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Flowhandler.""" + + cameras = [] + + async def async_step_init(self, user_input=None): + """Show form.""" + entity_reg: EntityRegistry = er.async_get(self.hass) + Items: EntityRegistryItems = entity_reg.entities + for item in Items.values(): + if item.domain == "camera" and item.disabled is False: + self.cameras.append(item.entity_id) + + data_schema = { + vol.Required(CONF_NAME): str, + vol.Required(CONF_ENTITY_ID, default=False): selector.SelectSelector( + selector.SelectSelectorConfig( + options=self.cameras, mode=selector.SelectSelectorMode.DROPDOWN + ), + ), + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(data_schema)) + + +@callback +def async_get_options_flow(config_entry): + """Optionflow callback.""" + return OptionsFlowHandler(config_entry) + + +class MissingentityidValue(exceptions.HomeAssistantError): + """Error to indicate entity_id is missing.""" + + +class MissingNameValue(exceptions.HomeAssistantError): + """Error to indicate entity_id is missing.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate entity_id is missing.""" diff --git a/custom_components/seven_segments_alt/const.py b/custom_components/seven_segments_alt/const.py new file mode 100644 index 0000000..eec07d9 --- /dev/null +++ b/custom_components/seven_segments_alt/const.py @@ -0,0 +1,13 @@ +"""Const.""" + +DOMAIN = "seven_segments_alt" +DATA_COORDINATOR = "seven_coordinator" +SS_DIGITS = "digits" +SS_EXTRA_ARGUMENTS = "extra_arguments" +SS_HEIGHT = "height" +SS_ROTATE = "rotate" +SS_THRESHOLD = "threshold" +SS_WIDTH = "width" +SS_X_POS = "x_position" +SS_Y_POS = "y_position" +SS_CAM = "camera_entity_id" diff --git a/custom_components/seven_segments_alt/entity.py b/custom_components/seven_segments_alt/entity.py new file mode 100644 index 0000000..bd270e4 --- /dev/null +++ b/custom_components/seven_segments_alt/entity.py @@ -0,0 +1,23 @@ +"""Base SS entity.""" +from __future__ import annotations + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import SSDataCoordinator + + +class SSEntity(CoordinatorEntity[SSDataCoordinator]): + """Base SS entity.""" + + def __init__( + self, + coordinator: SSDataCoordinator, + ) -> None: + """Initialize light.""" + super().__init__(coordinator) + self._attr_has_entity_name = False + self._attr_device_info = coordinator.device_info + + self._attr_unique_id = ( + f"{coordinator.unique_id}-{self.__class__.__name__.lower()}" + ) diff --git a/custom_components/seven_segments_alt/image.py b/custom_components/seven_segments_alt/image.py new file mode 100644 index 0000000..c3473b2 --- /dev/null +++ b/custom_components/seven_segments_alt/image.py @@ -0,0 +1,153 @@ +"""Support for SS.""" + +from __future__ import annotations + +import io +import logging + +from PIL import Image + +from homeassistant.components.image import ImageEntity +from homeassistant.core import HomeAssistant + +from . import SSDataCoordinator +from .const import DOMAIN, SS_HEIGHT, SS_WIDTH, SS_X_POS, SS_Y_POS +from .entity import SSEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: + """Do setup entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SSImage(hass, coordinator, "SS Image", "ss_image")]) + async_add_entities([SSImage_Pro(hass, coordinator, "SS Image pro", "ss_image_pro")]) + async_add_entities( + [SSImage_Crop(hass, coordinator, "SS Image crop", "ss_image_crop")] + ) + + +class SSImage_Crop(SSEntity, ImageEntity): + """SS Image Pro.""" + + data_coordinator: SSDataCoordinator + + def __init__( + self, + hass: HomeAssistant, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + self.hass = hass + super().__init__(coordinator) + ImageEntity.__init__(self, hass, False) + self.data_coordinator = coordinator + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self.jname = name + self.data_coordinator.image_entity_3 = self + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + try: # noqa: SIM105 + _LOGGER.debug(self.data_coordinator.processed_name) + stream = io.BytesIO(self.data_coordinator.ocr_image.content) + img = Image.open(stream) + # img = Image.open(self.data_coordinator.img_path, mode="r") + # width, height = img.size + roi_img = img.crop( + ( + self.data_coordinator.jdata[SS_X_POS], + self.data_coordinator.jdata[SS_Y_POS], + self.data_coordinator.jdata[SS_X_POS] + + self.data_coordinator.jdata[SS_WIDTH], + self.data_coordinator.jdata[SS_Y_POS] + + self.data_coordinator.jdata[SS_HEIGHT], + ) + ) # convert("RGB") + img_byte_arr = io.BytesIO() + roi_img.save(img_byte_arr, format="PNG") + img_byte_arr = img_byte_arr.getvalue() + + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug(ex) + return None + return img_byte_arr + + +class SSImage_Pro(SSEntity, ImageEntity): + """SS Image Pro.""" + + data_coordinator: SSDataCoordinator + + def __init__( + self, + hass: HomeAssistant, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + self.hass = hass + super().__init__(coordinator) + ImageEntity.__init__(self, hass, False) + self.data_coordinator = coordinator + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self.jname = name + self.data_coordinator.image_entity = self + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + try: # noqa: SIM105 + _LOGGER.debug(self.data_coordinator.processed_name) + img = Image.open(self.data_coordinator.processed_name, mode="r") + roi_img = img.convert("RGB") + img_byte_arr = io.BytesIO() + roi_img.save(img_byte_arr, format="PNG") + img_byte_arr = img_byte_arr.getvalue() + + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug(ex) + return None + return img_byte_arr + + +class SSImage(SSEntity, ImageEntity): + """SS Image.""" + + data_coordinator: SSDataCoordinator + + def __init__( + self, + hass: HomeAssistant, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + self.hass = hass + super().__init__(coordinator) + ImageEntity.__init__(self, hass, False) + self.data_coordinator = coordinator + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self.jname = name + self.data_coordinator.image_entity_2 = self + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + try: + _LOGGER.debug("New ss image") + return self.data_coordinator.ocr_image.content + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug(ex) + return None diff --git a/custom_components/seven_segments_alt/image_processing.py b/custom_components/seven_segments_alt/image_processing.py new file mode 100644 index 0000000..1ecb66c --- /dev/null +++ b/custom_components/seven_segments_alt/image_processing.py @@ -0,0 +1,195 @@ +"""Optical character recognition processing of seven segments displays.""" +from __future__ import annotations + +import io +import logging +import os +import subprocess + +from PIL import Image + +from homeassistant.components.image_processing import ( + ImageProcessingDeviceClass, + ImageProcessingEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import SSDataCoordinator +from .const import ( + DATA_COORDINATOR, + DOMAIN, + SS_DIGITS, + SS_EXTRA_ARGUMENTS, + SS_HEIGHT, + SS_ROTATE, + SS_THRESHOLD, + SS_WIDTH, + SS_X_POS, + SS_Y_POS, +) +from .entity import SSEntity + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_BINARY = "ssocr" + + +# async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Seven segments OCR platform.""" + coordinator: SSDataCoordinator = hass.data[DOMAIN][DATA_COORDINATOR] + entities = [] + entities.append( + ImageProcessingSsocr( + hass, coordinator.camera_entity_id, coordinator, "img_ssocr", "ss_img_ssocr" + ) + ) + async_add_entities(entities) + + +class ImageProcessingSsocr(SSEntity, ImageProcessingEntity): + """Representation of the seven segments OCR image processing entity.""" + + _attr_device_class = ImageProcessingDeviceClass.OCR + + def __init__( + self, + hass: HomeAssistant, + camera_entity, + coordinator: SSDataCoordinator, + name, + translationkey, + ) -> None: + """Initialize seven segments processing.""" + super().__init__(coordinator) + self.hass = hass + self.data_coordinator: SSDataCoordinator = coordinator + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + + self._camera_entity = camera_entity + self._state = None + self.data_coordinator.ocr_entity = self + self._attr_device_class = ImageProcessingDeviceClass.OCR + + self.filepath = os.path.join( + self.hass.config.config_dir, + "ssocr-{}.png".format( + (self.data_coordinator.componentname + "_" + name).replace(" ", "_") + ), + ) + self.data_coordinator.img_path = self.filepath + crop = [ + "crop", + str(self.data_coordinator.jdata[SS_X_POS]), + str(self.data_coordinator.jdata[SS_Y_POS]), + str(self.data_coordinator.jdata[SS_WIDTH]), + str(self.data_coordinator.jdata[SS_HEIGHT]), + ] + digits = ["-d", str(self.data_coordinator.jdata[SS_DIGITS])] + rotate = ["rotate", str(self.data_coordinator.jdata[SS_ROTATE])] + threshold = ["-t", str(self.data_coordinator.jdata[SS_THRESHOLD])] + extra_arguments = self.data_coordinator.jdata[SS_EXTRA_ARGUMENTS].split(" ") + + self._command = ( + [DEFAULT_BINARY] + + crop + + digits + + threshold + + rotate + + [self.data_coordinator.mandatory_extras] + + extra_arguments + ) + self._command.append(self.filepath) + _LOGGER.debug(self._command) + + async def set_command(self): + """Update command params.""" + crop = [ + "crop", + str(self.data_coordinator.jdata[SS_X_POS]), + str(self.data_coordinator.jdata[SS_Y_POS]), + str(self.data_coordinator.jdata[SS_WIDTH]), + str(self.data_coordinator.jdata[SS_HEIGHT]), + ] + digits = ["-d", str(self.data_coordinator.jdata[SS_DIGITS])] + rotate = ["rotate", str(self.data_coordinator.jdata[SS_ROTATE])] + threshold = ["-t", str(self.data_coordinator.jdata[SS_THRESHOLD])] + extra_arguments = self.data_coordinator.jdata[SS_EXTRA_ARGUMENTS].split(" ") + + self._command = ( + [DEFAULT_BINARY] + + crop + + digits + + threshold + + rotate + + [self.data_coordinator.mandatory_extras] + + extra_arguments + ) + self._command.append(self.filepath) + _LOGGER.debug(self._command) + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera_entity + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + async def async_update(self) -> None: + """Update image and process it. + + This method is a coroutine. + """ + camera = self.hass.components.camera + + try: + image: Image = await camera.async_get_image( + self.camera_entity, timeout=self.timeout + ) + self.data_coordinator.ocr_image = image + # image_last_updated = datetime.now() + except HomeAssistantError as err: + _LOGGER.debug("Error on receive image from entity: %s", err) + return + + # process image data + await self.async_process_image(image.content) + + def process_image(self, image): + """Process the image.""" + stream = io.BytesIO(image) + img = Image.open(stream) + img.save(self.filepath, "png") + + with subprocess.Popen( + self._command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False, # Required for posix_spawn + ) as ocr: + out = ocr.communicate() + if out[0] != b"": + self._state = out[0].strip().decode("utf-8") + else: + self._state = None + _LOGGER.debug( + "Unable to detect value: %s", out[1].strip().decode("utf-8") + ) + self.data_coordinator.ocr_state = self._state + self.data_coordinator.new_image = True + _LOGGER.debug("New image") diff --git a/custom_components/seven_segments_alt/manifest.json b/custom_components/seven_segments_alt/manifest.json new file mode 100644 index 0000000..2546fd5 --- /dev/null +++ b/custom_components/seven_segments_alt/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "seven_segments_alt", + "name": "Seven Segments OCR", + "codeowners": ["@Sdahl1234"], + "documentation": "https://github.com/Sdahl1234/seven_segments_alt", + "issue_tracker": "https://github.com/Sdahl1234/seven_segments_alt/issues", + "iot_class": "local_polling", + "requirements": ["Pillow==10.2.0"], + "config_flow": true, + "version": "1.0.0" +} diff --git a/custom_components/seven_segments_alt/number.py b/custom_components/seven_segments_alt/number.py new file mode 100644 index 0000000..14d420e --- /dev/null +++ b/custom_components/seven_segments_alt/number.py @@ -0,0 +1,60 @@ +"""Support for SS.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.core import HomeAssistant + +from . import SSDataCoordinator +from .const import DOMAIN +from .entity import SSEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: + """Do setup entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([SSNumber(coordinator, "x_position", "ss_x_position")]) + async_add_entities([SSNumber(coordinator, "y_position", "ss_y_position")]) + async_add_entities([SSNumber(coordinator, "height", "ss_height")]) + async_add_entities([SSNumber(coordinator, "width", "ss_width")]) + async_add_entities([SSNumber(coordinator, "rotate", "ss_rotate")]) + async_add_entities([SSNumber(coordinator, "threshold", "ss_threshold")]) + async_add_entities([SSNumber(coordinator, "digits", "ss_digits")]) + + +class SSNumber(SSEntity, NumberEntity): + """SS number.""" + + def __init__( + self, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + super().__init__(coordinator) + self.data_coordinator = coordinator + self.jname = name + self.native_step = 1 + self.native_max_value = 5000 + self.native_min_value = -100 + self.mode = NumberMode.BOX + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + self.data_coordinator.jdata[self.jname] = int(value) + await self.data_coordinator.ocr_entity.set_command() + + @property + def native_value(self): + """Return value.""" + return self.data_coordinator.jdata[self.jname] diff --git a/custom_components/seven_segments_alt/sensor.py b/custom_components/seven_segments_alt/sensor.py new file mode 100644 index 0000000..d37fb0a --- /dev/null +++ b/custom_components/seven_segments_alt/sensor.py @@ -0,0 +1,53 @@ +"""Sensor.""" +# import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.core import HomeAssistant + +from . import SSDataCoordinator +from .const import DOMAIN +from .entity import SSEntity + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """Async Setup entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices( + [ + ssSensor( + coordinator, + "OCR", + "ss_ocr", + "mdi:counter", + ) + ] + ) + + +class ssSensor(SSEntity, SensorEntity): + """SS sensor.""" + + def __init__( + self, coordinator: SSDataCoordinator, name: str, translationkey: str, icon: str + ) -> None: + """Init.""" + super().__init__(coordinator) + self.data_coordinator = coordinator + self._tk = translationkey + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self._attr_icon = icon + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will reflect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + return True + + @property + def state(self): # noqa: C901 + """State.""" + # Hent data fra data_handler her + return self.data_coordinator.ocr_state diff --git a/custom_components/seven_segments_alt/text.py b/custom_components/seven_segments_alt/text.py new file mode 100644 index 0000000..627c159 --- /dev/null +++ b/custom_components/seven_segments_alt/text.py @@ -0,0 +1,52 @@ +"""Support for SS.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.text import TextEntity +from homeassistant.core import HomeAssistant + +from . import SSDataCoordinator +from .const import DOMAIN +from .entity import SSEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: + """Do setup entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SSText(coordinator, "extra_arguments", "ss_extra_arguments")]) + + +# async_add_entities([SSText(coordinator, "camera_entity_id", "ss_camera_entity_id")]) + + +class SSText(SSEntity, TextEntity): + """SS text.""" + + def __init__( + self, + coordinator: SSDataCoordinator, + name: str, + translationkey: str, + ) -> None: + """Init.""" + super().__init__(coordinator) + self.data_coordinator = coordinator + self._attr_translation_key = translationkey + self._attr_has_entity_name = True + self._tk = translationkey + self._attr_unique_id = f"{name}_{self.data_coordinator.componentname}" + self.mode = "text" + self.jname = name + + async def async_set_value(self, value: str) -> None: + """Set the text value.""" + self.data_coordinator.jdata[self.jname] = value + + @property + def native_value(self): + """Return value.""" + return self.data_coordinator.jdata[self.jname] diff --git a/custom_components/seven_segments_alt/translations/en.json b/custom_components/seven_segments_alt/translations/en.json new file mode 100644 index 0000000..a75939c --- /dev/null +++ b/custom_components/seven_segments_alt/translations/en.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "connection_error": "Failed to connect" + }, + "step": { + "user": { + "data": { + "name": "Device name", + "entity_id": "Camera" + } + } + } + }, + "entity": { + "sensor": { + "ss_ocr": { + "name": "OCR" + } + }, + "image": { + "ss_image": { + "name": "Camera Image" + }, + "ss_image_pro": { + "name": "OCR Image" + }, + "ss_image_crop": { + "name": "Carmra crop Image" + } + }, + "image_processing": { + "ss_img_ssocr": { + "name": "OCR Unit" + } + }, + "button": { + "ss_save_data": { + "name": "Load data" + }, + "ss_load_data": { + "name": "Save data" + } + }, + "number": { + "ss_x_position": { + "name": "X position" + }, + "ss_y_position": { + "name": "y position" + }, + "ss_height": { + "name": "Height" + }, + "ss_width": { + "name": "Width" + }, + "ss_rotate": { + "name": "Rotate" + }, + "ss_threshold": { + "name": "Threshold" + }, + "ss_digits": { + "name": "Digits" + } + }, + "text": { + "ss_extra_arguments": { + "name": "Extra arguments" + } + } + } +} diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..e69de29