From 0328d26eda484fbb29c961a2b30ef705e163561e Mon Sep 17 00:00:00 2001 From: TheRealKillaruna Date: Sat, 30 Dec 2023 10:45:13 +0100 Subject: [PATCH] Import project --- .pre-commit-config.yaml | 58 ++++++++++ README.md | 3 + custom_components/__init__.py | 0 custom_components/pjlink2/__init__.py | 1 + custom_components/pjlink2/const.py | 21 ++++ custom_components/pjlink2/manifest.json | 11 ++ custom_components/pjlink2/sensor.py | 143 ++++++++++++++++++++++++ hacs.json | 5 + requirements.test.txt | 3 + setup.cfg | 62 ++++++++++ tests/__init__.py | 0 tests/bandit.yaml | 17 +++ tests/test_init.py | 9 ++ 13 files changed, 333 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 custom_components/__init__.py create mode 100644 custom_components/pjlink2/__init__.py create mode 100644 custom_components/pjlink2/const.py create mode 100644 custom_components/pjlink2/manifest.json create mode 100644 custom_components/pjlink2/sensor.py create mode 100644 hacs.json create mode 100644 requirements.test.txt create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/bandit.yaml create mode 100644 tests/test_init.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9931e3a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v2.3.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ + - repo: https://github.com/codespell-project/codespell + rev: v1.16.0 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.csv,*.json" + - --quiet-level=2 + exclude_types: [csv, json] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.1 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.0.2 + files: ^(homeassistant|script|tests)/.+\.py$ + - repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-executables-have-shebangs + stages: [manual] + - id: check-json + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.770 + hooks: + - id: mypy + args: + - --pretty + - --show-error-codes + - --show-error-context diff --git a/README.md b/README.md new file mode 100644 index 0000000..d55d2fd --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# PJLink2 for Home Assistant + +## Installation diff --git a/custom_components/__init__.py b/custom_components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/pjlink2/__init__.py b/custom_components/pjlink2/__init__.py new file mode 100644 index 0000000..23d0d09 --- /dev/null +++ b/custom_components/pjlink2/__init__.py @@ -0,0 +1 @@ +"""PJLink2 Custom Component.""" \ No newline at end of file diff --git a/custom_components/pjlink2/const.py b/custom_components/pjlink2/const.py new file mode 100644 index 0000000..4b01083 --- /dev/null +++ b/custom_components/pjlink2/const.py @@ -0,0 +1,21 @@ +"""Provides the constants needed for component.""" +from enum import StrEnum + +DOMAIN = "pjlink2" + +CONF_ENCODING = "encoding" +DEFAULT_ENCODING = "utf-8" +DEFAULT_PORT = 4352 +DEFAULT_TIMEOUT = 4 + +ATTR_PRODUCT_NAME = "product_name" +ATTR_MANUFACTURER_NAME = "manufacturer_name" +ATTR_PROJECTOR_NAME = "projector_name" +ATTR_RESOLUTION_X = "x_resolution" +ATTR_RESOLUTION_Y = "y_resolution" + +class ProjectorState(StrEnum): + OFF = "off" + ON = "on" + COOLING = "cooling" + WARMING = "warming" \ No newline at end of file diff --git a/custom_components/pjlink2/manifest.json b/custom_components/pjlink2/manifest.json new file mode 100644 index 0000000..4c4baac --- /dev/null +++ b/custom_components/pjlink2/manifest.json @@ -0,0 +1,11 @@ +{ + "codeowners": ["@TheRealKillaruna"], + "config_flow": false, + "dependencies": [], + "documentation": "https://github.com/TheRealKillaruna/pjlink2", + "domain": "pjlink2", + "iot_class": "calculated", + "name": "PJLink2", + "requirements": ["aiopjlink==1.0.5"], + "version": "0.1" +} diff --git a/custom_components/pjlink2/sensor.py b/custom_components/pjlink2/sensor.py new file mode 100644 index 0000000..5c236a7 --- /dev/null +++ b/custom_components/pjlink2/sensor.py @@ -0,0 +1,143 @@ +"""GitHub sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import logging +from typing import Any + +from aiopjlink import PJLink, PJLinkException, PJLinkProjectorError, Power, Sources, Lamp, Information + +from homeassistant import config_entries, core +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME, CONF_PASSWORD, CONF_TIMEOUT + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, HomeAssistantType + +import voluptuous as vol + +from .const import DOMAIN, CONF_ENCODING, DEFAULT_ENCODING, DEFAULT_PORT, DEFAULT_TIMEOUT, ATTR_PRODUCT_NAME, ATTR_MANUFACTURER_NAME, ATTR_PROJECTOR_NAME, ATTR_RESOLUTION_X, ATTR_RESOLUTION_Y, ProjectorState + + +_LOGGER = logging.getLogger(__name__) +# Time between updating data from projector +SCAN_INTERVAL = timedelta(seconds=3) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + vol.Optional(CONF_PASSWORD) : cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT) : cv.positive_float + } +) + + +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the sensor platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + password = config.get(CONF_PASSWORD) + timeout = config.get(CONF_TIMEOUT) + name = config.get(CONF_NAME) + pjl = PJLink(host, port, password, timeout) + sensors = [PJLink2Sensor(pjl, name)] + async_add_entities(sensors, update_before_add=True) + + +class PJLink2Sensor(Entity): + """Representation of a PJLink2 sensor.""" + + def __init__(self, pjl, name): + super().__init__() + self._projector = pjl + self.attrs: dict[str, Any] = {} + self._name = name + self._state = None + self._available = False + + async def async_will_remove_from_hass(self) -> None: + """Close connection.""" + await super().async_will_remove_from_hass() + try: + await self._projector.__aexit__(0,0,0) + except PJLinkException as err: + _LOGGER.exception("PJLink2 ERROR for %s: %s", self._name, repr(err)) + else: + _LOGGER.info("PJLink2 INFO for %s: Connection closed.", self._name) + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return self._projector._address + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def state(self) -> str | None: + return self._state + + @property + def extra_state_attributes(self) -> dict[str, Any]: + return self.attrs + + async def async_update(self) -> None: + """Update all sensors.""" + try: + if not self._available: + # connect and init static information + await self._projector.__aenter__() + self._available = True + info = await Information(self._projector).table() + self.attrs[ATTR_PRODUCT_NAME] = info["product_name"] + self.attrs[ATTR_MANUFACTURER_NAME] = info["manufacturer_name"] + self.attrs[ATTR_PROJECTOR_NAME] = info["projector_name"] + if self._name == None: self._name = info["projector_name"] + _LOGGER.info("PJLink2 INFO for %s: Connection opened.", self._name) + + pwr = await Power(self._projector).get() + if pwr == Power.State.OFF: self._state = ProjectorState.OFF + elif pwr == Power.State.ON: self._state = ProjectorState.ON + elif pwr == Power.State.COOLING: self._state = ProjectorState.COOLING + elif pwr == Power.State.WARMING: self._state = ProjectorState.WARMING + + if pwr==Power.ON: + res = await Sources(self._projector).resolution() + self.attrs[ATTR_RESOLUTION_X] = res[0] + self.attrs[ATTR_RESOLUTION_Y] = res[1] + else: + if ATTR_RESOLUTION_X in self.attrs: del self.attrs[ATTR_RESOLUTION_X] + if ATTR_RESOLUTION_Y in self.attrs: del self.attrs[ATTR_RESOLUTION_Y] + + except PJLinkProjectorError: + # resolution cannot be queried due to no input + if ATTR_RESOLUTION_X in self.attrs: del self.attrs[ATTR_RESOLUTION_X] + if ATTR_RESOLUTION_Y in self.attrs: del self.attrs[ATTR_RESOLUTION_Y] + _LOGGER.info("PJLink2 INFO for %s: Cannot get resolution", self._name) + except PJLinkException as err: + self._state = None + self._available = False + _LOGGER.exception("PJLink2 ERROR for %s: %s", self._name, repr(err)) + try: + await self._projector.__aexit__(0,0,0) + except PJLinkException as err: + _LOGGER.exception("PJLink2 ERROR for %s: %s", self._name, repr(err)) + else: + _LOGGER.info("PJLink2 INFO for %s: Connection closed.", self._name) diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..38a26f5 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "PJLink2", + "render_readme": true, + "iot_class": "calculated" +} diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..efeebaa --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,3 @@ +pytest +pytest-cov==2.9.0 +pytest-homeassistant-custom-component diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6197927 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,62 @@ +[coverage:run] +source = + custom_components + +[coverage:report] +exclude_lines = + pragma: no cover + raise NotImplemented() + if __name__ == '__main__': + main() +show_missing = true + +[tool:pytest] +testpaths = tests +norecursedirs = .git +addopts = + --strict + --cov=custom_components + +[flake8] +# https://github.com/ambv/black#line-length +max-line-length = 88 +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +# W504 line break after binary operator +ignore = + E501, + W503, + E203, + D202, + W504 + +[isort] +# https://github.com/timothycrosley/isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +# splits long import on multiple lines indented by 4 spaces +multi_line_output = 3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 +indent = " " +# by default isort don't check module indexes +not_skip = __init__.py +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +default_section = THIRDPARTY +known_first_party = custom_components,tests +forced_separate = tests +combine_as_imports = true + +[mypy] +python_version = 3.7 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/bandit.yaml b/tests/bandit.yaml new file mode 100644 index 0000000..ebd284e --- /dev/null +++ b/tests/bandit.yaml @@ -0,0 +1,17 @@ +# https://bandit.readthedocs.io/en/latest/config.html + +tests: + - B108 + - B306 + - B307 + - B313 + - B314 + - B315 + - B316 + - B317 + - B318 + - B319 + - B320 + - B325 + - B602 + - B604 diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..67da959 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,9 @@ +"""Test component setup.""" +from homeassistant.setup import async_setup_component + +from custom_components.pjlink2.const import DOMAIN + + +async def test_async_setup(hass): + """Test the component gets setup.""" + assert await async_setup_component(hass, DOMAIN, {}) is True