TheRealKillaruna преди 2 години
ревизия
0328d26eda

+ 58 - 0
.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

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# PJLink2 for Home Assistant
+
+## Installation

+ 0 - 0
custom_components/__init__.py


+ 1 - 0
custom_components/pjlink2/__init__.py

@@ -0,0 +1 @@
+"""PJLink2 Custom Component."""

+ 21 - 0
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"

+ 11 - 0
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"
+}

+ 143 - 0
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)

+ 5 - 0
hacs.json

@@ -0,0 +1,5 @@
+{
+  "name": "PJLink2",
+  "render_readme": true,
+  "iot_class": "calculated"
+}

+ 3 - 0
requirements.test.txt

@@ -0,0 +1,3 @@
+pytest
+pytest-cov==2.9.0
+pytest-homeassistant-custom-component

+ 62 - 0
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

+ 0 - 0
tests/__init__.py


+ 17 - 0
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

+ 9 - 0
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