From 40050941a9aa05d7afddfb126935e1ec7972d94f Mon Sep 17 00:00:00 2001 From: AdamNaj Date: Mon, 13 Jan 2020 12:22:54 +0100 Subject: [PATCH] Code commit & HACS integration. --- CONTRIBUTING.md | 50 +++++++ LICENSE | 2 +- README.md | 42 ++++++ custom_components/linksys_velop/__init__.py | 1 + .../linksys_velop/device_tracker.py | 125 ++++++++++++++++++ custom_components/linksys_velop/manifest.json | 9 ++ hacs.json | 3 + info.md | 25 ++++ 8 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md create mode 100644 custom_components/linksys_velop/__init__.py create mode 100644 custom_components/linksys_velop/device_tracker.py create mode 100644 custom_components/linksys_velop/manifest.json create mode 100644 hacs.json create mode 100644 info.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fc902d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contribution guidelines + +Contributing to this project should be as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features + +## Github is used for everything + +Github is used to host code, to track issues and feature requests, as well as accept pull requests. + +Pull requests are the best way to propose changes to the codebase. + +1. Fork the repo and create your branch from `master`. +2. If you've changed something, update the documentation. +3. Make sure your code lints (using black). +4. Issue that pull request! + +## Any contributions you make will be under the MIT Software License + +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using Github's [issues](../../issues) + +GitHub issues are used to track public bugs. +Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! + +## Write bug reports with detail, background, and sample code + +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can. +- What you expected would happen +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + +People *love* thorough bug reports. I'm not even kidding. + +## Use a Consistent Coding Style + +Use [black](https://github.com/ambv/black) to make sure the code follows the style. + +## License + +By contributing, you agree that your contributions will be licensed under its Apache 2.0 License. diff --git a/LICENSE b/LICENSE index 261eeb9..7540cf0 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 Adam Najmanowicz Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 4ff797a..a277cc9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ # linksys_velop The linksys_velop platform allows for presence detection by listing devices connected to your Linksys Velop router. + +It was tested with a Linksys Velop WHW03v1 Firmware version 1.1.11.197735 + +## Installation + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +2. If you do not have a `custom_components` directory (folder) there, you need to create it. +3. In the `custom_components` directory (folder) create a new folder called `linksys_velop`. +4. Download _all_ the files from the `custom_components/linksys_velop/` directory (folder) in this repository. +5. Place the files you downloaded in the new directory (folder) you created. +6. Restart Home Assistant. +7. Move on to the configuration. + +Using your HA configuration directory (folder) as a starting point you should now also have this: + +```text +custom_components/linksys_velop/__init__.py +custom_components/linksys_velop/device_tracker.py +custom_components/linksys_velop/manifest.json +``` + +## Example configuration.yaml + +```yaml +device_tracker: + - platform: linksys_velop + host: 192.168.1.1 + username: admin + password: YOUR_PASSWORD +``` + +### Configuration options + +Key | Type | Required | Description +-- | -- | -- | -- +`host` | `string` | `True` | The hostname or IP address of your access point, e.g., 192.168.1.1. +`username` | `string` | `False` | Defaults to `admin`. You should not have to customize it as Velops deveult to `admin` on login and only allow you to specify password. +`password` | `string` | `True` | The password for your given local admin account. + +## Contributions are welcome! + +If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) diff --git a/custom_components/linksys_velop/__init__.py b/custom_components/linksys_velop/__init__.py new file mode 100644 index 0000000..f1c0026 --- /dev/null +++ b/custom_components/linksys_velop/__init__.py @@ -0,0 +1 @@ +"""The linksys_velop component.""" diff --git a/custom_components/linksys_velop/device_tracker.py b/custom_components/linksys_velop/device_tracker.py new file mode 100644 index 0000000..4eb4544 --- /dev/null +++ b/custom_components/linksys_velop/device_tracker.py @@ -0,0 +1,125 @@ +"""Support for Linksys Velop routers.""" +import logging + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from base64 import b64encode as b64 + +DEFAULT_TIMEOUT = 10 + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME, default="admin"): cv.string, + } +) + + +def get_scanner(hass, config): + """Validate the configuration and return a Linksys AP scanner.""" + try: + return LinksysSmartWifiDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None + + +class LinksysSmartWifiDeviceScanner(DeviceScanner): + """This class queries a Linksys Access Point.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + self.last_results = {} + + # Check if the access point is accessible + response = self._make_request() + if not response.status_code == 200: + raise ConnectionError("Cannot connect to Linksys Access Point") + + def scan_devices(self): + """Scan for new devices and return a list with device IDs (MACs).""" + self._update_info() + + return self.last_results.keys() + + def get_device_name(self, device): + """Return the name (if known) of the device.""" + return self.last_results.get(device) + + def _update_info(self): + """Check for connected devices.""" + _LOGGER.info("Checking Linksys Smart Wifi") + + self.last_results = {} + response = self._make_request() + if response.status_code != 200: + _LOGGER.error( + "Got HTTP status code %d when getting device list", response.status_code + ) + return False + try: + data = response.json() + result = data["responses"][0] + devices = result["output"]["devices"] + for device in devices: + macs = device["knownMACAddresses"] + if not macs: + _LOGGER.warning("Skipping device without known MAC address") + continue + mac = macs[-1] + connections = device["connections"] + if not connections: + _LOGGER.debug("Device %s is not connected", mac) + continue + + name = None + for prop in device["properties"]: + if prop["name"] == "userDeviceName": + name = prop["value"] + if not name: + name = device.get("friendlyName", device["deviceID"]) + + _LOGGER.debug("Device %s is connected", mac) + self.last_results[mac] = name + except (KeyError, IndexError): + _LOGGER.exception("Router returned unexpected response") + return False + return True + + def _make_request(self): + # Weirdly enough, this doesn't seem to require authentication + data = [ + { + "request": {"sinceRevision": 0}, + "action": "http://linksys.com/jnap/devicelist/GetDevices", + } + ] + + token = b64(bytes(self.username + ":" + self.password, "utf8")).decode("ascii"); + + headers = { + "X-JNAP-Action": "http://linksys.com/jnap/core/Transaction", + "X-JNAP-Authorization": "Basic " + token + } + + + return requests.post( + f"http://{self.host}/JNAP/", + timeout=DEFAULT_TIMEOUT, + headers=headers, + json=data, + ) diff --git a/custom_components/linksys_velop/manifest.json b/custom_components/linksys_velop/manifest.json new file mode 100644 index 0000000..aa38599 --- /dev/null +++ b/custom_components/linksys_velop/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "linksys_velop", + "name": "Linksys Velop", + "documentation": "https://www.home-assistant.io/components/linksys_velop", + "requirements": [], + "dependencies": [], + "codeowners": ["@adamnaj"], + "homeassistant": "0.100.0" +} diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..a22bf77 --- /dev/null +++ b/hacs.json @@ -0,0 +1,3 @@ +{ + "name": "linksys_velop" +} \ No newline at end of file diff --git a/info.md b/info.md new file mode 100644 index 0000000..c2b0e71 --- /dev/null +++ b/info.md @@ -0,0 +1,25 @@ +# linksys_velop +The linksys_velop platform allows for presence detection by listing devices connected to your Linksys Velop router. + +It was tested with a Linksys Velop WHW03v1 Firmware version 1.1.11.197735 + +## Example configuration.yaml + +```yaml +device_tracker: + - platform: linksys_velop + host: 192.168.1.1 + password: YOUR_PASSWORD +``` + +### Configuration options + +Key | Type | Required | Description +-- | -- | -- | -- +`host` | `string` | `True` | The hostname or IP address of your access point, e.g., 192.168.1.1. +`username` | `string` | `False` | Defaults to `admin`. You should not have to customize it as Velops deveult to `admin` on login and only allow you to specify password. +`password` | `string` | `True` | The password for your given local admin account. + +## Contributions are welcome! + +If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)