From 7147b394a46df643ca9e5013d00605cab5baf983 Mon Sep 17 00:00:00 2001 From: Fabian Schultis Date: Sat, 2 Apr 2022 21:42:28 +0200 Subject: [PATCH] Initial commit --- .github/workflows/create_release.yml | 37 ++++ .gitignore | 1 + LICENSE | 21 +++ README.md | 3 + custom_components/jablotron_rs485/__init__.py | 16 ++ custom_components/jablotron_rs485/api.py | 74 ++++++++ .../jablotron_rs485/config_flow.py | 30 ++++ custom_components/jablotron_rs485/const.py | 16 ++ .../jablotron_rs485/manifest.json | 17 ++ .../jablotron_rs485/strings.json | 22 +++ custom_components/jablotron_rs485/switch.py | 159 ++++++++++++++++++ .../jablotron_rs485/translations/de.json | 23 +++ .../jablotron_rs485/translations/en.json | 23 +++ hacs.json | 6 + pyproject.toml | 35 ++++ 15 files changed, 483 insertions(+) create mode 100644 .github/workflows/create_release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom_components/jablotron_rs485/__init__.py create mode 100644 custom_components/jablotron_rs485/api.py create mode 100644 custom_components/jablotron_rs485/config_flow.py create mode 100644 custom_components/jablotron_rs485/const.py create mode 100644 custom_components/jablotron_rs485/manifest.json create mode 100644 custom_components/jablotron_rs485/strings.json create mode 100644 custom_components/jablotron_rs485/switch.py create mode 100644 custom_components/jablotron_rs485/translations/de.json create mode 100644 custom_components/jablotron_rs485/translations/en.json create mode 100644 hacs.json create mode 100644 pyproject.toml diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml new file mode 100644 index 0000000..5bd8739 --- /dev/null +++ b/.github/workflows/create_release.yml @@ -0,0 +1,37 @@ +name: 🚀 Create release + +on: + push: + branches: + - main + paths: + - custom_components/** + - pyproject.toml + - .github/** + +jobs: + create_release: + runs-on: ubuntu-latest + steps: + - name: ⚙️ Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: 🔃 Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: ✅ Hassfest validation + uses: home-assistant/actions/hassfest@master + + - name: ✅ HACS validation + uses: hacs/action@main + with: + category: integration + + - name: 📢 Semantic Release + uses: relekang/python-semantic-release@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..273845a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Pavel Roslovets + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..65cc5d6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Jablotron RS485 Home Assistant Integration + +For connecting to a JA-121-T using a Modbus to Ethernet adapter. diff --git a/custom_components/jablotron_rs485/__init__.py b/custom_components/jablotron_rs485/__init__.py new file mode 100644 index 0000000..177c612 --- /dev/null +++ b/custom_components/jablotron_rs485/__init__.py @@ -0,0 +1,16 @@ +import logging + +PLATFORMS = ["switch"] + +_LOGGER = logging.getLogger(__name__) + +async def async_setup(hass, config): + """Setup Jablotron RS485.""" + _LOGGER.info("Setting up.") + return True + +async def async_setup_entry(hass, config_entry): + """Set up entry.""" + _LOGGER.info("Initializing config entry.") + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + return True \ No newline at end of file diff --git a/custom_components/jablotron_rs485/api.py b/custom_components/jablotron_rs485/api.py new file mode 100644 index 0000000..44d4973 --- /dev/null +++ b/custom_components/jablotron_rs485/api.py @@ -0,0 +1,74 @@ +import asyncio +import logging, os +from requests.adapters import HTTPAdapter +from urllib3.util import Retry +import json +import time +from telnetlib import Telnet + +from .const import ( + CONF_HOST, + CONF_PORT, + CONF_PIN, + CONF_PGCOUNT, + CMD_PGSTATE, + CMD_PGON, + CMD_PGOFF, + OBJ_PG +) + +class JablotronRS485TerminalAPI: + def __init__(self, hass, config): + self._hass = hass + self._config = config + self._logger = logging.getLogger(__name__ + ":" + self.__class__.__name__) + + async def _request(self, command, _object, index, wait): + try: + with Telnet(self._config.get(CONF_HOST), self._config.get(CONF_PORT)) as tn: + tn.write(bytes(self._config.get(CONF_PIN)+" "+command+" "+str(index)+"\n",'UTF-8')) + if not wait: + tn.close() + return None + arrived = [False, False] + result = False + while not arrived[0] or not arrived[1]: + line = tn.read_until(b"\n") + msg = line.decode("utf-8").replace("\n", "").strip() + if (msg.replace(":", "") == command): + arrived[0] = True + if (arrived[0]): + splitted = msg.split(" ") + if (splitted[0] == _object): + arrived[1] = True + if (splitted[2] == "ON"): + result = True + elif (splitted[2] == "OFF"): + result = False + tn.close() + return result + except Exception as e: + self._logger.error("Error while executing telnet query:") + self._logger.error(e) + return None + + async def sleep(self, _time): + def _sleep(): + time.sleep(_time) + await self._hass.async_add_executor_job(_sleep) + + async def getDevices(self, _type): + devices = [] + for i in range(1, self._config.get(CONF_PGCOUNT) + 1): + devices.append(i) + + return devices + + async def getPGState(self, index): + return (await self._request(CMD_PGSTATE, OBJ_PG, index, True)) + + async def setPGState(self, index, state): + if (state): + return (await self._request(CMD_PGON, OBJ_PG, index, False)) + else: + return (await self._request(CMD_PGOFF, OBJ_PG, index, False)) diff --git a/custom_components/jablotron_rs485/config_flow.py b/custom_components/jablotron_rs485/config_flow.py new file mode 100644 index 0000000..40ee08b --- /dev/null +++ b/custom_components/jablotron_rs485/config_flow.py @@ -0,0 +1,30 @@ +from homeassistant import config_entries +from .const import DOMAIN +import voluptuous as vol +import logging + +_LOGGER = logging.getLogger(__name__) + +class JablotronRS485ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Example config flow.""" + + data = None + + async def async_step_user(self, info): + """Config flow step user.""" + if info is not None: + if not info.get("host") is None and not info.get("port") is None and not info.get("pin") is None and not info.get("pgcount") is None: + self.data = info + return await self.async_step_finish() + + return self.async_show_form( + step_id="user", data_schema=vol.Schema({ + vol.Required("host"): str, + vol.Required("port"): int, + vol.Required("pin"): str, + vol.Required("pgcount"): int, + }) + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry(title="Jablotron RS485", data=self.data) diff --git a/custom_components/jablotron_rs485/const.py b/custom_components/jablotron_rs485/const.py new file mode 100644 index 0000000..e6a9ba5 --- /dev/null +++ b/custom_components/jablotron_rs485/const.py @@ -0,0 +1,16 @@ +"""Constants for the Jablotron integration.""" + +DOMAIN = "jablotron_rs485" + +CONF_HOST = "host" +CONF_PORT = "port" +CONF_PIN = "pin" +CONF_PGCOUNT = "pgcount" + +CMD_PGSTATE = "PGSTATE" +CMD_PGON = "PGON" +CMD_PGOFF = "PGOFF" + +OBJ_PG = "PG" + +ATTR_DEVICE_TYPE_SWITCH = "SWITCH" diff --git a/custom_components/jablotron_rs485/manifest.json b/custom_components/jablotron_rs485/manifest.json new file mode 100644 index 0000000..0fd1cc3 --- /dev/null +++ b/custom_components/jablotron_rs485/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "jablotron_rs485", + "name": "Jablotron", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/jablotron", + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [ + "http" + ], + "codeowners": [ + "@asterix11" + ], + "requirements": [], + "iot_class": "cloud_push" +} diff --git a/custom_components/jablotron_rs485/strings.json b/custom_components/jablotron_rs485/strings.json new file mode 100644 index 0000000..98ff45e --- /dev/null +++ b/custom_components/jablotron_rs485/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Set up Jablotron", + "description": "Connect your Jablotron System using an RS485-Terminal-IP-Gateway.", + "data": { + "host": "Host/IP", + "port": "Port number", + "pin": "User PIN" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} \ No newline at end of file diff --git a/custom_components/jablotron_rs485/switch.py b/custom_components/jablotron_rs485/switch.py new file mode 100644 index 0000000..f659f85 --- /dev/null +++ b/custom_components/jablotron_rs485/switch.py @@ -0,0 +1,159 @@ +"""Support for lights through the Jablotron API.""" +from __future__ import annotations + +from collections.abc import Sequence + +import logging +import colorsys +import asyncio + +from homeassistant.components.switch import ( + SwitchEntity +) +import homeassistant.util.color as color_util +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, +) +from homeassistant.util import Throttle +from datetime import timedelta + +from .api import JablotronRS485TerminalAPI +from .const import ATTR_DEVICE_TYPE_SWITCH, DOMAIN + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the WiZ Light platform from legacy config.""" + + return True + + +async def async_setup_entry(hass, config_entry, async_add_devices): + try: + api = JablotronRS485TerminalAPI(hass, config_entry.data) + device_registry = await hass.helpers.device_registry.async_get_registry() + + devices = await api.getDevices(ATTR_DEVICE_TYPE_SWITCH) + for i in devices: + try: + async_add_devices( + [JablotronRS485Switch(api, i)], + update_before_add=True, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_UPNP, str(i))}, + identifiers={(DOMAIN, str(i))}, + manufacturer="Jablotron", + name="PG" + str(i), + model="Switch V1.0", + sw_version=0.2, + ) + except Exception as e: + _LOGGER.error("Can't add Jablotron Switch with ID %s.", str(i)) + _LOGGER.error(e) + except Exception as e: + _LOGGER.error("Can't add Jablotron Switches:") + _LOGGER.error(e) + return False + + return True + + +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: + """Return all capabilities supported if minimum required are present.""" + supported = [ + Capability.switch, + ] + return supported + + +class JablotronRS485Switch(SwitchEntity): + """Define a Jablotron Switch.""" + + def __init__(self, api, device_id): + """Initialize a Jablotron Switch.""" + self._lock = asyncio.Lock() + self._api = api + self._device_id = str(device_id) + self._name = "PG" + str(device_id) + self._mac = None + self._brightness = 0 + self._hs_color = [0, 0] + self._state = False + self._supported_features = self._determine_features() + self._logger = logging.getLogger( + ("%s:%s:<%s>") % (__name__, self.__class__.__name__, self._device_id) + ) + + def _determine_features(self): + """Get features supported by the device.""" + return None + + async def async_turn_on(self, **kwargs) -> None: + """Turn the light on.""" + try: + await self._lock.acquire() + self._logger.info("On") + if not self._state: + await self._api.setPGState(self._device_id, True) + self._state = True + self.async_schedule_update_ha_state(True) + finally: + self._lock.release() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the light off.""" + try: + await self._lock.acquire() + self._logger.info("Off") + self._state = False + await self._api.setPGState(self._device_id, False) + self.async_schedule_update_ha_state(True) + finally: + self._lock.release() + + async def async_update(self): + """Update entity attributes when the device status has changed.""" + self._logger.info( + "HIER PASSIERT FIESES ZEUGS! Diesmal mit jenem Jablotron Switch: %s", + self._device_id, + ) + + self._state = await self._api.getPGState(self._device_id) + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, self.unique_id) + }, + #"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, + "name": self.name, + "manufacturer": "Jablotron", + "model": "PG", + "sw_version": 0.2, + } + + @property + def unique_id(self): + """Return light unique_id.""" + return self._device_id + + @property + def name(self): + """Return the ip as name of the device if any.""" + return self._name + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._state + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features diff --git a/custom_components/jablotron_rs485/translations/de.json b/custom_components/jablotron_rs485/translations/de.json new file mode 100644 index 0000000..d4b28a9 --- /dev/null +++ b/custom_components/jablotron_rs485/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Jablotron integration is already configured for this email. Access token has been refreshed." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host/IP", + "port": "Port number", + "pin": "User PIN" + }, + "description": "Connect using the Host/IP and port.", + "title": "Connect to Jablotron" + } + } + } +} \ No newline at end of file diff --git a/custom_components/jablotron_rs485/translations/en.json b/custom_components/jablotron_rs485/translations/en.json new file mode 100644 index 0000000..d4b28a9 --- /dev/null +++ b/custom_components/jablotron_rs485/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Jablotron integration is already configured for this email. Access token has been refreshed." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host/IP", + "port": "Port number", + "pin": "User PIN" + }, + "description": "Connect using the Host/IP and port.", + "title": "Connect to Jablotron" + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..02e62f4 --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Jablotron RS485", + "domains": ["switch"], + "homeassistant": "2021.11.0", + "render_readme": true +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3617aff --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[tool.poetry] +name = "jablotron_rs485" +version = "1.0.0" +description = "Jablotron RS485 Home Assistant Integration" +authors = ["Asterix11 "] +license = "MIT" +readme = "README.md" +homepage = "https://github.com/asterix11/jablotron-rs485" +repository = "https://github.com/asterix11/jablotron-rs485.git" +keywords = ["Jablotron", "RS485", "HASS", "Home Assistant"] +classifiers =[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" + ] + +[tool.poetry.dependencies] +python = ">=3.9" + +[tool.poetry.dev-dependencies] +homeassistant = "^2021.11.0" +sp110e = "^1.4.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.semantic_release] +remove_dist = "false" +branch = "main" +version_toml = "pyproject.toml:tool.poetry.version" +version_pattern = 'custom_components/jablotron_rs485/manifest.json:\"version\": "(\d+\.\d+\.\d+)"' +upload_to_release = false +upload_to_pypi = false +build_command = false