diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..0844926 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,17 @@ +name: Validate + +on: + push: + pull_request: +# schedule: +# - cron: "0 0 * * *" + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v2" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44866c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +.idea/ +.homeassistant/ diff --git a/README.md b/README.md index 0b9b01e..22ea9de 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -# Dataplicity integration for Home Assistant +# Dataplicity for Home Assistant [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) -[![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://www.buymeacoffee.com/AlexxIT) -[![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001428278477) Custom component for public HTTPS access to [Home Assistant](https://www.home-assistant.io/) with [Dataplicity](https://www.dataplicity.com/) service. @@ -10,18 +8,28 @@ Should work on any Linux PC or ARM, not only Raspberry as Dataplicity service sa With free Dataplicity subscription - limited to only one server. -But if you have an extra $5 per month - it's better to use [Nabu Casa](https://www.nabucasa.com/about/) service for public HTTPS access to Home Assistant. In this way you can support the core developers of Home Assistant. +**ATTENTION:** I am in no way connected with the creators of this service. + +**ATTENTION:** Dataplicity **will be able to access all information that flows through their servers** if they want to. Use at your own risk. Read more: [github][1], [hass community][2], [raspberry forum][3] + +[1]: https://github.com/hacs/default/pull/940#issuecomment-840687472 +[2]: https://community.home-assistant.io/t/hass-io-add-on-webhook-relay-webhook-forwarding-remote-access/70961/2 +[3]: https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=117100 + +If you have an extra $5 per month - it's better to use [Nabu Casa](https://www.nabucasa.com/about/) service for public HTTPS access to Home Assistant. In this way you can support the core developers of Home Assistant. Also they uses TLS pass-through, so your data will be secure. -## Install +## Installation + +**Method 1.** [HACS](https://hacs.xyz/) custom repo: -You can install component with HACS custom repo ([example](https://github.com/AlexxIT/SonoffLAN#install-with-hacs)): `AlexxIT/Dataplicity`. +> HACS > Integrations > 3 dots (upper top corner) > Custom repositories > URL: `AlexxIT/Dataplicity`, Category: Integration > Add > wait > Dataplicity > Install -Or manually copy `dataplicity` folder from [latest release](https://github.com/AlexxIT/Dataplicity/releases/latest) to `custom_components` folder in your config folder. +**Method 2.** Manually copy `dataplicity` folder from [latest release](https://github.com/AlexxIT/Dataplicity/releases/latest) to `/config/custom_components` folder. -# Config +## Configuration -With GUI: Configuration > Integrations > Plus > Dataplicity > Follow instructions. +> Configuration > Integrations > Add Integration > **Dataplicity** If the integration is not in the list, you need to clear the browser cache. diff --git a/custom_components/dataplicity/__init__.py b/custom_components/dataplicity/__init__.py index e7dc006..5055061 100644 --- a/custom_components/dataplicity/__init__.py +++ b/custom_components/dataplicity/__init__.py @@ -1,3 +1,4 @@ +import inspect from threading import Thread from homeassistant.config_entries import ConfigEntry @@ -6,21 +7,34 @@ from homeassistant.requirements import async_process_requirements from homeassistant.util import package -DOMAIN = 'dataplicity' +from . import utils + +DOMAIN = "dataplicity" async def async_setup(hass: HomeAssistant, hass_config: dict): - # fix problems with `enum34==1000000000.0.0` constraint in Hass real_install = package.install_package - def fake_install(*args, **kwargs): - kwargs.pop('constraints') - return real_install(*args, **kwargs) + def fake_install(pkg: str, *args, **kwargs): + if pkg == "dataplicity==0.4.40": + return utils.install_package(pkg, *args, **kwargs) + return real_install(pkg, *args, **kwargs) try: package.install_package = fake_install + # latest dataplicity has bug with redirect_port - await async_process_requirements(hass, DOMAIN, ['dataplicity==0.4.40']) + await async_process_requirements(hass, DOMAIN, ["dataplicity==0.4.40"]) + + # fix Python 3.11 support + if not hasattr(inspect, "getargspec"): + + def getargspec(*args): + spec = inspect.getfullargspec(*args) + return spec.args, spec.varargs, spec.varkw, spec.defaults + + inspect.getargspec = getargspec + return True except: return False @@ -29,16 +43,14 @@ def fake_install(*args, **kwargs): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - # fix: module 'platform' has no attribute 'linux_distribution' - from dataplicity import device_meta - device_meta.get_os_version = lambda: "Linux" - - from dataplicity.client import Client + # fix https://github.com/AlexxIT/Dataplicity/issues/29 + Client = await hass.async_add_executor_job(utils.import_client) - hass.data[DOMAIN] = client = Client(serial=entry.data['serial'], - auth_token=entry.data['auth']) + hass.data[DOMAIN] = client = Client( + serial=entry.data["serial"], auth_token=entry.data["auth"] + ) # replace default 80 port to Hass port (usual 8123) - client.port_forward.add_service('web', hass.config.api.port) + client.port_forward.add_service("web", hass.config.api.port) Thread(name=DOMAIN, target=client.run_forever).start() async def hass_stop(event): @@ -46,13 +58,12 @@ async def hass_stop(event): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_stop) + await utils.fix_middleware(hass) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - from dataplicity.client import Client - - client: Client = hass.data[DOMAIN] + client = hass.data[DOMAIN] client.exit() - return True diff --git a/custom_components/dataplicity/config_flow.py b/custom_components/dataplicity/config_flow.py index 6f991e2..f038dfd 100644 --- a/custom_components/dataplicity/config_flow.py +++ b/custom_components/dataplicity/config_flow.py @@ -10,40 +10,41 @@ _LOGGER = logging.getLogger(__name__) -RE_TOKEN = re.compile(r'https://www\.dataplicity\.com/([a-z0-9]+)\.py') +RE_TOKEN = re.compile(r"https://www\.dataplicity\.com/([a-z0-9]+)\.py") class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user(self, data=None, error=None): - if sys.platform == 'win32': - return self.async_abort(reason='win32') + if sys.platform == "win32": + return self.async_abort(reason="win32") if self.hass.config.api.use_ssl: - return self.async_abort(reason='ssl') + return self.async_abort(reason="ssl") if data is None: return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required('token'): str, - }), - errors={'base': error} if error else None + step_id="user", + data_schema=vol.Schema( + { + vol.Required("token"): str, + } + ), + errors={"base": error} if error else None, ) - m = RE_TOKEN.search(data['token']) - token = m[1] if m else data['token'] + m = RE_TOKEN.search(data["token"]) + token = m[1] if m else data["token"] if not token.isalnum(): - return await self.async_step_user(error='token') + return await self.async_step_user(error="token") session = async_get_clientsession(self.hass) resp = await utils.register_device(session, token) if resp: - return self.async_create_entry(title="Dataplicity", data={ - 'auth': resp['auth'], - 'serial': resp['serial'] - }, description_placeholders={ - 'device_url': resp['device_url'] - }) - - return await self.async_step_user(error='auth') + return self.async_create_entry( + title="Dataplicity", + data={"auth": resp["auth"], "serial": resp["serial"]}, + description_placeholders={"device_url": resp["device_url"]}, + ) + + return await self.async_step_user(error="auth") diff --git a/custom_components/dataplicity/manifest.json b/custom_components/dataplicity/manifest.json index 754f37f..69cb192 100644 --- a/custom_components/dataplicity/manifest.json +++ b/custom_components/dataplicity/manifest.json @@ -1,15 +1,14 @@ { "domain": "dataplicity", "name": "Dataplicity", - "version": "1.0.0", - "config_flow": true, - "documentation": "https://github.com/AlexxIT/Dataplicity", - "issue_tracker": "https://github.com/AlexxIT/Dataplicity/issues", - "dependencies": [ - ], "codeowners": [ "@AlexxIT" ], - "requirements": [ - ] + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/AlexxIT/Dataplicity", + "iot_class": "cloud_push", + "issue_tracker": "https://github.com/AlexxIT/Dataplicity/issues", + "requirements": [], + "version": "1.2.1" } \ No newline at end of file diff --git a/custom_components/dataplicity/utils.py b/custom_components/dataplicity/utils.py index b32f57d..862c71e 100644 --- a/custom_components/dataplicity/utils.py +++ b/custom_components/dataplicity/utils.py @@ -1,15 +1,21 @@ import logging +import os +import sys +from ipaddress import IPv4Network +from subprocess import Popen, PIPE from aiohttp import ClientSession +from homeassistant.core import HomeAssistant _LOGGER = logging.getLogger(__name__) async def register_device(session: ClientSession, token: str): try: - r = await session.post('https://www.dataplicity.com/install/', data={ - 'name': "Home Assistant", 'serial': 'None', 'token': token - }) + r = await session.post( + "https://www.dataplicity.com/install/", + data={"name": "Home Assistant", "serial": "None", "token": token}, + ) if r.status != 200: _LOGGER.error(f"Can't register dataplicity device: {r.status}") return None @@ -17,3 +23,101 @@ async def register_device(session: ClientSession, token: str): except: _LOGGER.exception("Can't register dataplicity device") return None + + +async def fix_middleware(hass: HomeAssistant): + """Dirty hack for HTTP integration. Plug and play for usual users... + + [v2021.7] Home Assistant will now block HTTP requests when a misconfigured + reverse proxy, or misconfigured Home Assistant instance when using a + reverse proxy, has been detected. + + http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + """ + for f in hass.http.app.middlewares: + if f.__name__ != "forwarded_middleware": + continue + # https://til.hashrocket.com/posts/ykhyhplxjh-examining-the-closure + for i, var in enumerate(f.__code__.co_freevars): + cell = f.__closure__[i] + if var == "use_x_forwarded_for": + if not cell.cell_contents: + cell.cell_contents = True + elif var == "trusted_proxies": + if not cell.cell_contents: + cell.cell_contents = [IPv4Network("127.0.0.1/32")] + + +def install_package( + package: str, + upgrade: bool = True, + target: str | None = None, + constraints: str | None = None, + timeout: int | None = None, +) -> bool: + # important to use no-deps, because: + # - enum34 has problems with Hass constraints + # - six has problmes with Python 3.12 + args = [ + sys.executable, + "-m", + "pip", + "install", + "--quiet", + package, + "--no-deps", + # "enum34==1.1.6", + # "six==1.10.0", + "lomond==0.3.3", + ] + env = os.environ.copy() + + if timeout: + args += ["--timeout", str(timeout)] + if upgrade: + args.append("--upgrade") + if constraints is not None: + args += ["--constraint", constraints] + if target: + args += ["--user"] + env["PYTHONUSERBASE"] = os.path.abspath(target) + + _LOGGER.debug("Running pip command: args=%s", args) + + with Popen( + args, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + env=env, + close_fds=False, # required for posix_spawn + ) as process: + _, stderr = process.communicate() + if process.returncode != 0: + _LOGGER.error( + "Unable to install package %s: %s", + package, + stderr.decode("utf-8").lstrip().strip(), + ) + return False + + return True + + +def import_client(): + # fix: type object 'array.array' has no attribute 'tostring' + from dataplicity import iptool + + iptool.get_all_interfaces = lambda: [("lo", "127.0.0.1")] + + # fix: module 'platform' has no attribute 'linux_distribution' + from dataplicity import device_meta + + device_meta.get_os_version = lambda: "Linux" + + from dataplicity.client import Client + + return Client diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..b5357cb --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Dataplicity", + "render_readme": true +} \ No newline at end of file