├── .gitattributes ├── requirements_dev.txt ├── custom_components ├── palgate │ ├── pylgate │ │ ├── py.typed │ │ ├── __init__.py │ │ ├── types.py │ │ ├── _utils.py │ │ ├── token_generator.py │ │ ├── _constants.py │ │ └── _aes.py │ ├── manifest.json │ ├── const.py │ ├── __init__.py │ ├── strings.json │ ├── translations │ │ └── en.json │ ├── cover.py │ ├── api.py │ └── config_flow.py └── __init__.py ├── requirements_test.txt ├── .gitignore ├── hacs.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .devcontainer ├── configuration.yaml ├── devcontainer.json └── README.md ├── .github ├── workflows │ ├── cron.yml │ ├── push.yml │ └── pull.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md ├── setup.cfg ├── README.md ├── CONTRIBUTING.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | homeassistant 2 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom components module.""" 2 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-homeassistant-custom-component==0.13.214 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | pythonenv* 3 | venv 4 | .venv 5 | .coverage 6 | .idea 7 | .DS_Store -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/__init__.py: -------------------------------------------------------------------------------- 1 | from .token_generator import generate_token 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Palgate", 3 | "hacs": "1.6.0", 4 | "homeassistant": "2022.8.0", 5 | "render_readme": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.enabled": true, 4 | "python.pythonPath": "/usr/local/bin/python", 5 | "files.associations": { 6 | "*.yaml": "home-assistant" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | logger: 4 | default: info 5 | logs: 6 | custom_components.palgate: debug 7 | 8 | # If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) 9 | # debugpy: 10 | -------------------------------------------------------------------------------- /custom_components/palgate/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "palgate", 3 | "name": "Palgate", 4 | "codeowners": ["@ShonP40"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/ShonP40/ha-palgate", 7 | "iot_class": "cloud_polling", 8 | "issue_tracker": "https://github.com/ShonP40/ha-palgate/issues", 9 | "requirements": ["pyqrcode>=1.2.1","pypng"], 10 | "version": "1.3.1" 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron actions 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | validate: 9 | runs-on: "ubuntu-latest" 10 | name: Validate 11 | steps: 12 | - uses: "actions/checkout@v2" 13 | 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" 18 | ignore: brands 19 | 20 | - name: Hassfest validation 21 | uses: "home-assistant/actions/hassfest@master" 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /custom_components/palgate/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Palgate integration.""" 2 | 3 | from logging import Logger, getLogger 4 | 5 | LOGGER: Logger = getLogger(__package__) 6 | 7 | DOMAIN = "palgate" 8 | PLATFORMS = ["cover"] 9 | 10 | CONF_PHONE_NUMBER = "phone_number" 11 | CONF_TOKEN_TYPE = "token_type" 12 | 13 | CONF_ADVANCED = "advanced_options" 14 | CONF_SECONDS_TO_OPEN = "seconds_to_open" 15 | CONF_SECONDS_OPEN = "seconds_open" 16 | CONF_SECONDS_TO_CLOSE = "seconds_to_close" 17 | CONF_ALLOW_INVERT_AS_STOP = "allow_invert_as_stop" 18 | CONF_LINK_NEW_DEVICE = "Link New Device" 19 | 20 | SECONDS_TO_OPEN = 25 21 | SECONDS_OPEN = 45 22 | SECONDS_TO_CLOSE = 35 23 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | name: Validate 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | 16 | - name: HACS validation 17 | uses: "hacs/action@main" 18 | with: 19 | category: "integration" 20 | ignore: brands 21 | 22 | - name: Hassfest validation 23 | uses: "home-assistant/actions/hassfest@master" 24 | 25 | style: 26 | runs-on: "ubuntu-latest" 27 | name: Check style formatting 28 | steps: 29 | - uses: "actions/checkout@v2" 30 | - uses: "actions/setup-python@v1" 31 | with: 32 | python-version: "3.x" 33 | - run: python3 -m pip install black 34 | - run: black . 35 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | types used by pylgate 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from enum import IntEnum 18 | 19 | 20 | class TokenType(IntEnum): 21 | SMS = 0 22 | PRIMARY = 1 # Linked Device - first 23 | SECONDARY = 2 # Linked Device - second 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 9123", 6 | "type": "shell", 7 | "command": "container start", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "Run Home Assistant configuration against /config", 12 | "type": "shell", 13 | "command": "container check", 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "label": "Upgrade Home Assistant to latest dev", 18 | "type": "shell", 19 | "command": "container install", 20 | "problemMatcher": [] 21 | }, 22 | { 23 | "label": "Install a specific version of Home Assistant", 24 | "type": "shell", 25 | "command": "container set-version", 26 | "problemMatcher": [] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "image": "ludeeus/container:integration-debian", 4 | "name": "Flexit development", 5 | "context": "..", 6 | "appPort": [ 7 | "9123:8123" 8 | ], 9 | "postCreateCommand": "container install", 10 | "extensions": [ 11 | "ms-python.python", 12 | "github.vscode-pull-request-github", 13 | "ryanluker.vscode-coverage-gutters", 14 | "ms-python.vscode-pylance" 15 | ], 16 | "settings": { 17 | "files.eol": "\n", 18 | "editor.tabSize": 4, 19 | "terminal.integrated.shell.linux": "/bin/bash", 20 | "python.pythonPath": "/usr/bin/python3", 21 | "python.analysis.autoSearchPaths": false, 22 | "python.linting.pylintEnabled": true, 23 | "python.linting.enabled": true, 24 | "python.formatting.provider": "black", 25 | "editor.formatOnPaste": false, 26 | "editor.formatOnSave": true, 27 | "editor.formatOnType": true, 28 | "files.trimTrailingWhitespace": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | // Example of attaching to local debug server 7 | "name": "Python: Attach Local", 8 | "type": "python", 9 | "request": "attach", 10 | "port": 5678, 11 | "host": "localhost", 12 | "pathMappings": [ 13 | { 14 | "localRoot": "${workspaceFolder}", 15 | "remoteRoot": "." 16 | } 17 | ] 18 | }, 19 | { 20 | // Example of attaching to my production server 21 | "name": "Python: Attach Remote", 22 | "type": "python", 23 | "request": "attach", 24 | "port": 5678, 25 | "host": "homeassistant.local", 26 | "pathMappings": [ 27 | { 28 | "localRoot": "${workspaceFolder}", 29 | "remoteRoot": "/usr/src/homeassistant" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 16 | 17 | ## Version of the custom_component 18 | 21 | 22 | ## Configuration 23 | 24 | ```yaml 25 | 26 | Add your logs here. 27 | 28 | ``` 29 | 30 | ## Describe the bug 31 | A clear and concise description of what the bug is. 32 | 33 | 34 | ## Debug log 35 | 36 | 37 | 38 | ```text 39 | 40 | Add your logs here. 41 | 42 | ``` -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | doctests = True 4 | # To work with Black 5 | max-line-length = 88 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | 18 | [isort] 19 | # https://github.com/timothycrosley/isort 20 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 21 | # splits long import on multiple lines indented by 4 spaces 22 | multi_line_output = 3 23 | include_trailing_comma=True 24 | force_grid_wrap=0 25 | use_parentheses=True 26 | line_length=88 27 | indent = " " 28 | # by default isort don't check module indexes 29 | not_skip = __init__.py 30 | # will group `import x` and `from x import` of the same module. 31 | force_sort_within_sections = true 32 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 33 | default_section = THIRDPARTY 34 | known_first_party = custom_components.integration_blueprint, tests 35 | combine_as_imports = true 36 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | Warning: 18 | Contents of this module are intended to be used internally by the library and *not* by the 19 | user. Changes to this module are not considered breaking changes and may not be documented in 20 | the changelog. 21 | """ 22 | import ctypes 23 | 24 | 25 | def galois_mul2(value: ctypes.c_uint8) -> ctypes.c_uint8: 26 | if value.value >> 7: 27 | return ctypes.c_uint8((value.value << 1) ^ 0x1b) 28 | else: 29 | return ctypes.c_uint8(value.value << 1) 30 | 31 | 32 | def bytes_to_uint8(data: bytes) -> [ctypes.c_uint8]: 33 | return [ctypes.c_uint8(i) for i in data] 34 | 35 | 36 | def uint8_to_bytes(data: [ctypes.c_uint8]) -> bytes: 37 | return bytes(i.value for i in data) 38 | -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Pull actions 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | validate: 8 | runs-on: "ubuntu-latest" 9 | name: Validate 10 | steps: 11 | - uses: "actions/checkout@v2" 12 | 13 | - name: HACS validation 14 | uses: "hacs/action@main" 15 | with: 16 | category: "integration" 17 | ignore: brands 18 | 19 | - name: Hassfest validation 20 | uses: "home-assistant/actions/hassfest@master" 21 | 22 | style: 23 | runs-on: "ubuntu-latest" 24 | name: Check style formatting 25 | steps: 26 | - uses: "actions/checkout@v2" 27 | - uses: "actions/setup-python@v1" 28 | with: 29 | python-version: "3.x" 30 | - run: python3 -m pip install black 31 | - run: black . 32 | 33 | tests: 34 | runs-on: "ubuntu-latest" 35 | name: Run tests 36 | steps: 37 | - name: Check out code from GitHub 38 | uses: "actions/checkout@v2" 39 | - name: Setup Python 40 | uses: "actions/setup-python@v1" 41 | with: 42 | python-version: "3.x" 43 | - name: Install requirements 44 | run: python3 -m pip install -r requirements_test.txt 45 | - name: Run tests 46 | run: | 47 | pytest \ 48 | -qq \ 49 | --timeout=9 \ 50 | --durations=10 \ 51 | -n auto \ 52 | --cov custom_components.integration_blueprint \ 53 | -o console_output_style=count \ 54 | -p no:sugar \ 55 | tests 56 | -------------------------------------------------------------------------------- /custom_components/palgate/__init__.py: -------------------------------------------------------------------------------- 1 | """The Palgate integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant 7 | 8 | from .const import DOMAIN as PALGATE_DOMAIN 9 | from .const import * 10 | 11 | 12 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 13 | """Set up Palgate from a config entry.""" 14 | 15 | 16 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 17 | 18 | return True 19 | 20 | 21 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 22 | """Unload a config entry.""" 23 | 24 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 25 | 26 | return unload_ok 27 | 28 | 29 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 30 | """Reload config entry.""" 31 | await async_unload_entry(hass, entry) 32 | await async_setup_entry(hass, entry) 33 | 34 | async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): 35 | 36 | if config_entry.version > 3: 37 | return False 38 | 39 | new_data = dict(config_entry.data) 40 | 41 | if config_entry.version < 2: 42 | 43 | new_data[CONF_TOKEN_TYPE] = "1" 44 | 45 | if config_entry.version < 3: 46 | 47 | new_data[CONF_ADVANCED] = { 48 | CONF_SECONDS_TO_OPEN : SECONDS_TO_OPEN, 49 | CONF_SECONDS_OPEN : SECONDS_OPEN, 50 | CONF_SECONDS_TO_CLOSE : SECONDS_TO_CLOSE, 51 | CONF_ALLOW_INVERT_AS_STOP : False 52 | } 53 | 54 | hass.config_entries.async_update_entry(config_entry, data=new_data, version=3) 55 | 56 | return True 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Palgate Home Assistant integration 2 | 3 | ![GitHub Release](https://img.shields.io/github/v/release/ShonP40/ha-palgate?style=flat-square) 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 5 | 6 | Unofficial integration, use at your own risk! 7 | 8 | ## Installation 9 | 10 | 1. Ensure that [HACS](https://hacs.xyz/) is installed 11 | 2. Add this repository as a custom repository 12 | 3. Search for and install the "Palgate" integration 13 | 4. Restart Home Assistant 14 | 5. Configure the `Palgate` integration 15 | 16 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=ShonP40&repository=ha-palgate&category=Integration) 17 | 18 | ## Configuration 19 | 20 | 1. `Device ID` - This is your physical Palgate device ID, can be obtained from the settings page of each gate in the Palgate app 21 | 2. `Linked Phone Number` - Lists the phone numbers of the Palgate accounts you linked 22 | 23 | ## Advanced Configuration 24 | 1. `Time (sec) gate takes to open` - The time it takes for the gate to open in seconds 25 | 2. `Time (sec) gate remains open` - The time the gate remains open in seconds 26 | 3. `Time (sec) gate takes to close` - The time it takes for the gate to close in seconds 27 | 4. `Allow triggering gate while opening, to invert direction` - If enabled, the gate can be triggered while opening to invert the direction 28 | 29 | ## Features 30 | ### Cover 31 | - Open 32 | - Custom open/close timouts 33 | 34 | ## Notes 35 | - Palgate's API does not report the position of the gate 36 | 37 | ## Credits 38 | - [sindrebroch](https://github.com/sindrebroch) - Original creator 39 | - [DonutByte](https://github.com/DonutByte) - [Python implementation with an updated time-sensitive token generator](https://github.com/DonutByte/pylgate) 40 | - [doron1](https://github.com/doron1) - [Implemented support for time-sensitive tokens](https://github.com/ShonP40/ha-palgate/pull/4) 41 | - [bondar](https://github.com/bondar) - [Implemented support for devices that control multiple gates](https://github.com/ShonP40/ha-palgate/pull/18) 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Contributing to this project should be as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Github is used for everything 11 | 12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | Pull requests are the best way to propose changes to the codebase. 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've changed something, update the documentation. 18 | 3. Make sure your code lints (using black). 19 | 4. Test you contribution. 20 | 5. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | 24 | In short, when you submit code changes, your submissions are understood to be under the same [Apache License 2.0 License](https://github.com/ShonP40/ha-palgate/blob/master/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](../../issues) 27 | 28 | GitHub issues are used to track public bugs. 29 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! 30 | 31 | ## Write bug reports with detail, background, and sample code 32 | 33 | **Great Bug Reports** tend to have: 34 | 35 | - A quick summary and/or background 36 | - Steps to reproduce 37 | - Be specific! 38 | - Give sample code if you can. 39 | - What you expected would happen 40 | - What actually happens 41 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 42 | 43 | People *love* thorough bug reports. I'm not even kidding. 44 | 45 | ## Use a Consistent Coding Style 46 | 47 | Use [black](https://github.com/ambv/black) to make sure the code follows the style. 48 | 49 | ## Test your code modification 50 | 51 | This custom component is based on [integration_blueprint template](https://github.com/custom-components/integration_blueprint). 52 | 53 | It comes with development environment in a container, easy to launch 54 | if you use Visual Studio Code. With this container you will have a stand alone 55 | Home Assistant instance running and already configured with the included 56 | [`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml) 57 | file. 58 | 59 | ## License 60 | 61 | By contributing, you agree that your contributions will be licensed under its MIT License. 62 | -------------------------------------------------------------------------------- /custom_components/palgate/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "device_id": "Device ID", 7 | "phone_number": "Linked Phone Number" 8 | }, 9 | "data_description": { 10 | "device_id": "Device ID of the gate, can be found in the Palgate app", 11 | "phone_number": "Phone number of Palgate Linked Device authorized for gate" 12 | }, 13 | "sections": { 14 | "advanced_options": { 15 | "name": "Advanced Options", 16 | "description": "If you are unsure, leave at default values", 17 | "data": { 18 | "seconds_to_open": "Time (sec) gate takes to open", 19 | "seconds_open": "Time (sec) gate remains open", 20 | "seconds_to_close": "Time (sec) gate takes to close", 21 | "allow_invert_as_stop": "Allow triggering gate while opening, to invert direction" 22 | }, 23 | "data_description": { 24 | "allow_invert_as_stop": "If selected, STOP will trigger the gate again during open" 25 | } 26 | } 27 | } 28 | }, 29 | "reconfigure": { 30 | "data": { 31 | "device_id": "Device ID" 32 | }, 33 | "data_description": { 34 | "device_id": "Device ID of the gate, can be found in the Palgate app" 35 | }, 36 | "sections": { 37 | "advanced_options": { 38 | "name": "Advanced Options", 39 | "description": "If you are unsure, leave at default values", 40 | "data": { 41 | "seconds_to_open": "Time (sec) gate takes to open", 42 | "seconds_open": "Time (sec) gate remains open", 43 | "seconds_to_close": "Time (sec) gate takes to close", 44 | "allow_invert_as_stop": "Allow triggering gate while opening, to invert direction" 45 | }, 46 | "data_description": { 47 | "allow_invert_as_stop": "If selected, STOP will trigger the gate again during open" 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | "error": { 54 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 55 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 56 | "unknown": "[%key:common::config_flow::error::unknown%]", 57 | "cant_reconfigure_device_id": "Device ID cannot be reconfigured. Update rejected" 58 | }, 59 | "abort": { 60 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", 61 | "reconfigure_successful": "Gate entry reconfigured succefully" 62 | }, 63 | "progress": { 64 | "wait_for_qr": "Please open the Palgate app on your phone, select LINK A DEVICE on the LINKED DEVICES page and then scan this QR code. \n\nWaiting...\n\n\n {qr_code}" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /custom_components/palgate/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured", 5 | "reconfigure_successful": "Gate entry reconfigured succefully" 6 | }, 7 | "error": { 8 | "cannot_connect": "Failed to connect", 9 | "invalid_auth": "Invalid authentication", 10 | "unknown": "Unexpected error", 11 | "cant_reconfigure_device_id": "Device ID cannot be reconfigured. Update rejected" 12 | }, 13 | "step": { 14 | "user": { 15 | "data": { 16 | "device_id": "Device ID", 17 | "phone_number": "Linked Phone Number" 18 | }, 19 | "data_description": { 20 | "device_id": "Device ID of the gate, can be found in the Palgate app", 21 | "phone_number": "Phone number of Palgate Linked Device authorized for gate" 22 | }, 23 | "sections": { 24 | "advanced_options": { 25 | "name": "Advanced Options", 26 | "description": "If you are unsure, leave at default values", 27 | "data": { 28 | "seconds_to_open": "Time (sec) gate takes to open", 29 | "seconds_open": "Time (sec) gate remains open", 30 | "seconds_to_close": "Time (sec) gate takes to close", 31 | "allow_invert_as_stop": "Allow triggering gate while opening, to invert direction" 32 | }, 33 | "data_description": { 34 | "allow_invert_as_stop": "If selected, STOP will trigger the gate again during open" 35 | } 36 | } 37 | } 38 | }, 39 | "reconfigure": { 40 | "data": { 41 | "device_id": "Device ID" 42 | }, 43 | "data_description": { 44 | "device_id": "Device ID of the gate, can be found in the Palgate app" 45 | }, 46 | "sections": { 47 | "advanced_options": { 48 | "name": "Advanced Options", 49 | "description": "If you are unsure, leave at default values", 50 | "data": { 51 | "seconds_to_open": "Time (sec) gate takes to open", 52 | "seconds_open": "Time (sec) gate remains open", 53 | "seconds_to_close": "Time (sec) gate takes to close", 54 | "allow_invert_as_stop": "Allow triggering gate while opening, to invert direction" 55 | }, 56 | "data_description": { 57 | "allow_invert_as_stop": "If selected, STOP will trigger the gate again during open" 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "progress": { 64 | "wait_for_qr": "Please open the Palgate app on your phone, select LINK A DEVICE on the LINKED DEVICES page and then scan this QR code. \n\nWaiting...\n\n\n {qr_code}" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /custom_components/palgate/cover.py: -------------------------------------------------------------------------------- 1 | """Sensor file for Palgate.""" 2 | 3 | from typing import Any, Optional 4 | 5 | from homeassistant.components.cover import ( 6 | CoverEntity, 7 | CoverEntityDescription, 8 | CoverDeviceClass, 9 | ) 10 | 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.const import CONF_DEVICE_ID, CONF_TOKEN 13 | from homeassistant.core import HomeAssistant 14 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 15 | from homeassistant.helpers.entity import DeviceInfo 16 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 17 | 18 | from .api import PalgateApiClient 19 | from .const import DOMAIN as PALGATE_DOMAIN 20 | from .const import * 21 | 22 | async def async_setup_entry( 23 | hass: HomeAssistant, 24 | entry: ConfigEntry, 25 | async_add_entities: AddEntitiesCallback, 26 | ) -> None: 27 | """Add Palgate entities from a config_entry.""" 28 | 29 | COVERS: tuple[CoverEntityDescription, ...] = ( 30 | CoverEntityDescription( 31 | key=entry.data[CONF_DEVICE_ID], 32 | name=entry.data[CONF_DEVICE_ID], 33 | icon="mdi:boom-gate-outline", 34 | device_class=CoverDeviceClass.GARAGE, 35 | ), 36 | ) 37 | 38 | api = PalgateApiClient( 39 | device_id=entry.data[CONF_DEVICE_ID], 40 | token=entry.data[CONF_TOKEN], 41 | token_type=entry.data[CONF_TOKEN_TYPE], 42 | phone_number=entry.data[CONF_PHONE_NUMBER], 43 | seconds_to_open=entry.data[CONF_ADVANCED][CONF_SECONDS_TO_OPEN], 44 | seconds_open=entry.data[CONF_ADVANCED][CONF_SECONDS_OPEN], 45 | seconds_to_close=entry.data[CONF_ADVANCED][CONF_SECONDS_TO_CLOSE], 46 | allow_invert_as_stop=entry.data[CONF_ADVANCED][CONF_ALLOW_INVERT_AS_STOP], 47 | session=async_get_clientsession(hass), 48 | ) 49 | 50 | async_add_entities( 51 | PalgateCover(api, description) for description in COVERS 52 | ) 53 | 54 | 55 | class PalgateCover(CoverEntity): 56 | """Define a Palgate entity.""" 57 | 58 | def __init__( 59 | self, 60 | api: PalgateApiClient, 61 | description: CoverEntityDescription, 62 | ) -> None: 63 | """Initialize.""" 64 | 65 | self.api = api 66 | self.entity_description = description 67 | 68 | self._attr_unique_id = f"{description.key}" 69 | self._attr_device_info = DeviceInfo( 70 | identifiers={(PALGATE_DOMAIN, "palgate")}, 71 | name="Palgate", 72 | model="Palgate", 73 | manufacturer="Palgate", 74 | ) 75 | 76 | @property 77 | def is_opening(self) -> Optional[bool]: 78 | """Return if the cover is opening or not.""" 79 | return self.api.is_opening() 80 | 81 | @property 82 | def is_closing(self) -> Optional[bool]: 83 | """Return if the cover is closing or not.""" 84 | return self.api.is_closing() 85 | 86 | @property 87 | def is_closed(self) -> Optional[bool]: 88 | """Return if the cover is closed or not.""" 89 | return self.api.is_closed() 90 | 91 | async def async_open_cover(self, **kwargs: Any) -> None: 92 | """Open the cover.""" 93 | 94 | await self.api.open_gate() 95 | 96 | async def async_stop_cover(self, **kwargs: Any) -> None: 97 | """Stop the cover - only if allowed in config (usually auto-close)""" 98 | 99 | await self.api.invert_gate() -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | ## Developing with Visual Studio Code + devcontainer 2 | 3 | The easiest way to get started with custom integration development is to use Visual Studio Code with devcontainers. This approach will create a preconfigured development environment with all the tools you need. 4 | 5 | In the container you will have a dedicated Home Assistant core instance running with your custom component code. You can configure this instance by updating the `./devcontainer/configuration.yaml` file. 6 | 7 | **Prerequisites** 8 | 9 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 10 | - Docker 11 | - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/) 12 | - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education. 13 | - [Visual Studio code](https://code.visualstudio.com/) 14 | - [Remote - Containers (VSC Extension)][extension-link] 15 | 16 | [More info about requirements and devcontainer in general](https://code.visualstudio.com/docs/remote/containers#_getting-started) 17 | 18 | [extension-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 19 | 20 | **Getting started:** 21 | 22 | 1. Fork the repository. 23 | 2. Clone the repository to your computer. 24 | 3. Open the repository using Visual Studio code. 25 | 26 | When you open this repository with Visual Studio code you are asked to "Reopen in Container", this will start the build of the container. 27 | 28 | _If you don't see this notification, open the command palette and select `Remote-Containers: Reopen Folder in Container`._ 29 | 30 | ### Tasks 31 | 32 | The devcontainer comes with some useful tasks to help you with development, you can start these tasks by opening the command palette and select `Tasks: Run Task` then select the task you want to run. 33 | 34 | When a task is currently running (like `Run Home Assistant on port 9123` for the docs), it can be restarted by opening the command palette and selecting `Tasks: Restart Running Task`, then select the task you want to restart. 35 | 36 | The available tasks are: 37 | 38 | Task | Description 39 | -- | -- 40 | Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`. 41 | Run Home Assistant configuration against /config | Check the configuration. 42 | Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch. 43 | Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container. 44 | 45 | ### Step by Step debugging 46 | 47 | With the development container, 48 | you can test your custom component in Home Assistant with step by step debugging. 49 | 50 | You need to modify the `configuration.yaml` file in `.devcontainer` folder 51 | by uncommenting the line: 52 | 53 | ```yaml 54 | # debugpy: 55 | ``` 56 | 57 | Then launch the task `Run Home Assistant on port 9123`, and launch the debugger 58 | with the existing debugging configuration `Python: Attach Local`. 59 | 60 | For more information, look at [the Remote Python Debugger integration documentation](https://www.home-assistant.io/integrations/debugpy/). 61 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/token_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logic for generating a derived token used by PalGate API 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import struct 18 | import time 19 | 20 | from ._constants import ( 21 | BLOCK_SIZE, 22 | T_C_KEY, 23 | TOKEN_SIZE, 24 | TIMESTAMP_OFFSET, 25 | ) 26 | 27 | from ._aes import aes_encrypt_decrypt 28 | 29 | from .types import TokenType 30 | 31 | 32 | def generate_token(session_token: bytes, 33 | phone_number: int, 34 | token_type: TokenType, 35 | *, 36 | timestamp_ms: int | None = None, 37 | timestamp_offset: int = TIMESTAMP_OFFSET) -> str: 38 | """Generates a derived token for PalGate API 39 | Args: 40 | session_token (bytes): Base token generated either via SMS or Device Linking. 41 | phone_number (int): The phone number associated with `session_token` in international format. 42 | PalGate uses phone numbers as user IDs, referring to each user's phone number as their user id 43 | token_type (TokenType): `session_token`'s token type 44 | timestamp_ms (:obj:`int`, optional): time in seconds since Epoch. Defaults to current time 45 | The derived token is based on this timestamp, each token is valid for approximately 5 seconds 46 | timestamp_offset (:obj:`int`, optional): offset to add to `timestamp_ms`. 47 | Defaults to `TIMESTAMP_OFFSET` which is the value used by PalGate's official app 48 | 49 | Returns: 50 | str: The derived token as hex string 51 | 52 | Raises: 53 | ValueError: if `session_token` is not 16 bytes. 54 | if `phone_number` is not 12 digits 55 | if `token_type`'s value does not exist 56 | """ 57 | if len(session_token) != BLOCK_SIZE: 58 | raise ValueError('Invalid session token') 59 | 60 | if timestamp_ms is None: 61 | timestamp_ms = int(time.time()) 62 | 63 | step_2_key = _step_1(session_token, phone_number) 64 | 65 | step_2_result = _step_2(step_2_key, timestamp_ms, timestamp_offset) 66 | 67 | result = bytearray(TOKEN_SIZE) 68 | if token_type == TokenType.SMS: 69 | result[0] = 0x01 70 | elif token_type == TokenType.PRIMARY: 71 | result[0] = 0x11 72 | elif token_type == TokenType.SECONDARY: 73 | result[0] = 0x21 74 | else: 75 | raise ValueError(f'unknown token type: {token_type}') 76 | 77 | result[1] = (phone_number >> 0x28) & 0xff 78 | result[2] = (phone_number >> 0x20) & 0xff 79 | result[3] = (phone_number >> 0x18) & 0xff 80 | result[4:7] = struct.pack(">Q", phone_number)[5:8] 81 | 82 | result[7:23] = step_2_result 83 | 84 | return result.hex().upper() 85 | 86 | 87 | def _step_1(session_token: bytes, phone_number: int) -> bytes: 88 | key = T_C_KEY.copy() 89 | key[6:12] = struct.pack('>Q', phone_number)[2:] 90 | 91 | return aes_encrypt_decrypt(session_token, bytes(key), is_encrypt=True) 92 | 93 | 94 | def _step_2(result_from_step_1: bytes, timestamp_ms: int, timestamp_offset: int) -> bytes: 95 | next_state = bytearray(BLOCK_SIZE) 96 | next_state[1:3] = struct.pack('I', timestamp_ms + timestamp_offset) 98 | 99 | return aes_encrypt_decrypt(bytes(next_state), result_from_step_1, is_encrypt=False) 100 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/_constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants used throughout the project 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | Warning: 18 | Contents of this module are intended to be used internally by the library and *not* by the 19 | user. Changes to this module are not considered breaking changes and may not be documented in 20 | the changelog. 21 | """ 22 | # AES constants 23 | S_BOX = [ 24 | 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 25 | 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 26 | 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 27 | 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 28 | 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 29 | 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 30 | 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 31 | 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 32 | 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 33 | 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 34 | 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 35 | 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 36 | 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 37 | 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 38 | 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 39 | 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16] 40 | 41 | INVERSE_S_BOX = [ 42 | 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 43 | 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 44 | 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 45 | 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 46 | 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 47 | 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 48 | 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 49 | 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 50 | 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 51 | 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 52 | 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 53 | 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 54 | 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 55 | 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 56 | 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 57 | 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d] 58 | 59 | RCON = [ 60 | 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36] 61 | 62 | BLOCK_SIZE = 16 63 | KEY_SIZE = 16 64 | 65 | # Palgate 66 | T_C_KEY = bytearray([0xfa, 0xd3, 0x25, 0x72, 0x81, 0x29, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0xB4, 0x5a, 0x65]) 68 | 69 | TOKEN_SIZE = 23 # In bytes 70 | TIMESTAMP_OFFSET = 2 71 | -------------------------------------------------------------------------------- /custom_components/palgate/api.py: -------------------------------------------------------------------------------- 1 | """Palgate library.""" 2 | 3 | from http import HTTPStatus 4 | import json 5 | from typing import Any, Optional 6 | 7 | from datetime import datetime, timedelta 8 | 9 | import aiohttp 10 | from voluptuous.error import Error 11 | 12 | from .pylgate.token_generator import generate_token 13 | 14 | class PalgateApiClient: 15 | """Main class for handling connection with.""" 16 | 17 | def __init__( 18 | self, 19 | device_id: str, 20 | token: str, 21 | token_type: str, 22 | phone_number: str, 23 | seconds_to_open: int, 24 | seconds_open: int, 25 | seconds_to_close: int, 26 | allow_invert_as_stop: bool, 27 | session: Optional[aiohttp.client.ClientSession] = None, 28 | ) -> None: 29 | """Initialize connection with Palgate.""" 30 | 31 | self._session = session 32 | self.device_id: str = device_id 33 | self.token: str = token 34 | self.token_type: str = token_type 35 | self.phone_number: str = phone_number 36 | self.seconds_to_open: int = seconds_to_open 37 | self.seconds_open: int = seconds_open 38 | self.seconds_to_close: int = seconds_to_close 39 | self.allow_invert_as_stop: bool = allow_invert_as_stop 40 | self.next_open: datetime = datetime.now() 41 | self.next_closing: datetime = datetime.now() 42 | self.next_closed: datetime = datetime.now() 43 | 44 | def url(self) -> str: 45 | """Build the url by extracting the gate number (:1 or :2, etc...) and set it in the outputNum""" 46 | device_id = self.device_id 47 | output_num = 1 # default 48 | 49 | if ':' in device_id: 50 | base_id, output = device_id.rsplit(':', 1) 51 | if output.isdigit(): 52 | device_id = base_id 53 | output_num = int(output) 54 | 55 | return f"https://api1.pal-es.com/v1/bt/device/{device_id}/open-gate?openBy=100&outputNum={output_num}" 56 | 57 | def headers(self) -> dict: 58 | """Get headers""" 59 | 60 | temporal_token = generate_token(bytes.fromhex(self.token),int(self.phone_number),int(self.token_type)) 61 | return { 62 | "Accept": "*/*", 63 | "Accept-Encoding": "gzip, deflate, br", 64 | "Accept-Language": "en-us", 65 | "Connection": "keep-alive", 66 | "Content-Type": "application/json", 67 | "User-Agent": "BlueGate/115 CFNetwork/1128.0.1 Darwin/19.6.0", 68 | "x-bt-token": f"{temporal_token}", 69 | } 70 | 71 | def is_opening(self) -> bool: 72 | """Current state of gate is opening.""" 73 | 74 | return True if (self.next_open > datetime.now()) else False 75 | 76 | def is_closing(self) -> bool: 77 | """Current state of gate is closing.""" 78 | 79 | return True if (self.next_closed > datetime.now() and self.next_closing < datetime.now()) else False 80 | 81 | 82 | def is_closed(self) -> bool: 83 | """Current state of gate is open.""" 84 | 85 | return False if (self.next_closed > datetime.now()) else True 86 | 87 | async def open_gate(self) -> Any: 88 | """Open Palgate device.""" 89 | 90 | async with self._session.get(url=self.url(), headers=self.headers()) as resp: 91 | if resp.status == HTTPStatus.UNAUTHORIZED: 92 | raise Error(f"Unauthorized. {resp.status}") 93 | if resp.status != HTTPStatus.OK: 94 | error_text = json.loads(await resp.text()) 95 | raise Error(f"Not OK {resp.status} {error_text}") 96 | 97 | self.next_open = datetime.now() + timedelta(seconds=self.seconds_to_open) 98 | self.next_closing = datetime.now() + timedelta(seconds=(self.seconds_to_open + self.seconds_open)) 99 | self.next_closed = datetime.now() + timedelta(seconds=(self.seconds_to_open + self.seconds_open + self.seconds_to_close)) 100 | 101 | return await resp.json() 102 | async def invert_gate(self) -> Any: 103 | """Trigger the Palgate device again during open""" 104 | 105 | if (self.allow_invert_as_stop and self.is_opening()): 106 | 107 | async with self._session.get(url=self.url(), headers=self.headers()) as resp: 108 | if resp.status == HTTPStatus.UNAUTHORIZED: 109 | raise Error(f"Unauthorized. {resp.status}") 110 | if resp.status != HTTPStatus.OK: 111 | error_text = json.loads(await resp.text()) 112 | raise Error(f"Not OK {resp.status} {error_text}") 113 | 114 | self.next_closing = datetime.now() 115 | self.next_closed = datetime.now() + timedelta(seconds=(self.seconds_to_close)) # Best guess 116 | 117 | return await resp.json() 118 | -------------------------------------------------------------------------------- /custom_components/palgate/pylgate/_aes.py: -------------------------------------------------------------------------------- 1 | """ 2 | AES 128-bit encryption and decryption 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | Warning: 18 | Contents of this module are intended to be used internally by the library and *not* by the 19 | user. Changes to this module are not considered breaking changes and may not be documented in 20 | the changelog. 21 | """ 22 | import ctypes 23 | 24 | from ._constants import S_BOX, RCON, INVERSE_S_BOX, BLOCK_SIZE, KEY_SIZE 25 | from ._utils import galois_mul2, bytes_to_uint8, uint8_to_bytes 26 | 27 | 28 | def aes_encrypt_decrypt(state: bytes, key: bytes, *, is_encrypt: bool) -> bytes: 29 | if len(state) != BLOCK_SIZE or len(key) != KEY_SIZE: 30 | raise ValueError("State and/or key are not 16 bytes") 31 | 32 | state_array = bytes_to_uint8(state) 33 | key_array = bytes_to_uint8(key) 34 | _aes_enc_dec(state_array, key_array, is_encrypt) 35 | 36 | return uint8_to_bytes(state_array) 37 | 38 | 39 | def _aes_enc_dec(state: [ctypes.c_uint8], key: [ctypes.c_uint8], encrypt: bool) -> None: 40 | if encrypt: 41 | for rnd in range(10): 42 | key[0] = ctypes.c_uint8(S_BOX[key[13].value] ^ key[0].value ^ RCON[rnd]) 43 | key[1] = ctypes.c_uint8(S_BOX[key[14].value] ^ key[1].value) 44 | key[2] = ctypes.c_uint8(S_BOX[key[15].value] ^ key[2].value) 45 | key[3] = ctypes.c_uint8(S_BOX[key[12].value] ^ key[3].value) 46 | for i in range(4, KEY_SIZE): 47 | key[i] = ctypes.c_uint8(key[i].value ^ key[i - 4].value) 48 | 49 | for i in range(BLOCK_SIZE): 50 | state[i] = ctypes.c_uint8(state[i].value ^ key[i].value) 51 | 52 | for rnd in range(10): 53 | if encrypt: 54 | for i in range(KEY_SIZE - 1, 3, -1): 55 | key[i] = ctypes.c_uint8(key[i].value ^ key[i - 4].value) 56 | key[0] = ctypes.c_uint8(S_BOX[key[13].value] ^ key[0].value ^ RCON[9 - rnd]) 57 | key[1] = ctypes.c_uint8(S_BOX[key[14].value] ^ key[1].value) 58 | key[2] = ctypes.c_uint8(S_BOX[key[15].value] ^ key[2].value) 59 | key[3] = ctypes.c_uint8(S_BOX[key[12].value] ^ key[3].value) 60 | else: 61 | for i in range(BLOCK_SIZE): 62 | state[i] = ctypes.c_uint8(S_BOX[state[i].value ^ key[i].value]) 63 | 64 | buf1 = state[1].value 65 | state[1].value = state[5].value 66 | state[5].value = state[9].value 67 | state[9].value = state[13].value 68 | state[13].value = buf1 69 | 70 | buf1, buf2 = state[2].value, state[6].value 71 | state[2].value = state[10].value 72 | state[6].value = state[14].value 73 | state[10].value = buf1 74 | state[14].value = buf2 75 | 76 | buf1 = state[15].value 77 | state[15].value = state[11].value 78 | state[11].value = state[7].value 79 | state[7].value = state[3].value 80 | state[3].value = buf1 81 | 82 | if (rnd > 0 and encrypt) or (rnd < 9 and not encrypt): 83 | for i in range(4): 84 | buf4 = i << 2 85 | if encrypt: 86 | buf1 = galois_mul2(galois_mul2(ctypes.c_uint8(state[buf4].value ^ state[buf4 + 2].value))).value 87 | buf2 = galois_mul2(galois_mul2(ctypes.c_uint8(state[buf4 + 1].value ^ state[buf4 + 3].value))).value 88 | state[buf4].value ^= buf1 89 | state[buf4 + 1].value ^= buf2 90 | state[buf4 + 2].value ^= buf1 91 | state[buf4 + 3].value ^= buf2 92 | 93 | buf1 = state[buf4].value ^ state[buf4 + 1].value ^ state[buf4 + 2].value ^ state[buf4 + 3].value 94 | buf2 = state[buf4].value 95 | buf3 = galois_mul2(ctypes.c_uint8(state[buf4].value ^ state[buf4 + 1].value)).value 96 | state[buf4].value = state[buf4].value ^ buf3 ^ buf1 97 | buf3 = galois_mul2(ctypes.c_uint8(state[buf4 + 1].value ^ state[buf4 + 2].value)).value 98 | state[buf4 + 1].value = state[buf4 + 1].value ^ buf3 ^ buf1 99 | buf3 = galois_mul2(ctypes.c_uint8(state[buf4 + 2].value ^ state[buf4 + 3].value)).value 100 | state[buf4 + 2].value = state[buf4 + 2].value ^ buf3 ^ buf1 101 | buf3 = galois_mul2(ctypes.c_uint8(state[buf4 + 3].value ^ buf2)).value 102 | state[buf4 + 3].value = state[buf4 + 3].value ^ buf3 ^ buf1 103 | 104 | if encrypt: 105 | buf1 = state[13].value 106 | state[13].value = state[9].value 107 | state[9].value = state[5].value 108 | state[5].value = state[1].value 109 | state[1].value = buf1 110 | 111 | buf1, buf2 = state[10].value, state[14].value 112 | state[10].value = state[2].value 113 | state[14].value = state[6].value 114 | state[2].value = buf1 115 | state[6].value = buf2 116 | 117 | buf1 = state[3].value 118 | state[3].value = state[7].value 119 | state[7].value = state[11].value 120 | state[11].value = state[15].value 121 | state[15].value = buf1 122 | 123 | for i in range(BLOCK_SIZE): 124 | state[i] = ctypes.c_uint8(INVERSE_S_BOX[state[i].value] ^ key[i].value) 125 | else: 126 | key[0] = ctypes.c_uint8(S_BOX[key[13].value] ^ key[0].value ^ RCON[rnd]) 127 | key[1] = ctypes.c_uint8(S_BOX[key[14].value] ^ key[1].value) 128 | key[2] = ctypes.c_uint8(S_BOX[key[15].value] ^ key[2].value) 129 | key[3] = ctypes.c_uint8(S_BOX[key[12].value] ^ key[3].value) 130 | for i in range(4, KEY_SIZE): 131 | key[i] = ctypes.c_uint8(key[i].value ^ key[i - 4].value) 132 | 133 | if not encrypt: 134 | for i in range(BLOCK_SIZE): 135 | state[i] = ctypes.c_uint8(state[i].value ^ key[i].value) 136 | -------------------------------------------------------------------------------- /custom_components/palgate/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Palgate integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | import logging 7 | import asyncio 8 | import uuid 9 | import pyqrcode 10 | import io 11 | import json 12 | import aiohttp 13 | from http import HTTPStatus 14 | 15 | import voluptuous as vol 16 | 17 | from homeassistant import config_entries 18 | from homeassistant.const import CONF_DEVICE_ID, CONF_TOKEN 19 | from homeassistant.data_entry_flow import FlowResult, section 20 | from homeassistant.helpers.selector import selector 21 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 22 | from homeassistant.exceptions import HomeAssistantError 23 | 24 | from .const import DOMAIN as PALGATE_DOMAIN 25 | from .const import * 26 | 27 | _LOGGER = logging.getLogger(__name__) 28 | 29 | class PollenvarselFlowHandler(config_entries.ConfigFlow, domain=PALGATE_DOMAIN): 30 | """Config flow for Palgate.""" 31 | 32 | VERSION = 3 33 | 34 | def __init__(self): 35 | 36 | self._task: asyncio.Task[None] | None = None 37 | self.linking_code: str = None 38 | 39 | async def async_step_user( 40 | self, user_input: dict[str, Any] | None = None 41 | ) -> FlowResult: 42 | """Handle a flow initialized by the user.""" 43 | 44 | if user_input is not None: 45 | 46 | device_id: str = user_input[CONF_DEVICE_ID] 47 | 48 | if await self._async_existing_devices(device_id): 49 | return self.async_abort(reason="already_configured") 50 | 51 | await self.async_set_unique_id(device_id) 52 | self._abort_if_unique_id_configured() 53 | 54 | self.user_input = user_input 55 | 56 | if user_input[CONF_PHONE_NUMBER] == CONF_LINK_NEW_DEVICE: 57 | return await self.async_step_create_linked_device( 58 | user_input=user_input, 59 | ) 60 | else: 61 | self._linked_phone_number = user_input[CONF_PHONE_NUMBER] 62 | self._linked_token = \ 63 | self.current_linked_devices[ 64 | user_input[CONF_PHONE_NUMBER]][0] 65 | self._linked_token_type = \ 66 | self.current_linked_devices[ 67 | user_input[CONF_PHONE_NUMBER]][1] 68 | 69 | return await self.async_step_complete_new_entry() 70 | 71 | return self.async_create_entry( 72 | title=device_id.title(), 73 | data=user_input, 74 | ) 75 | 76 | if self._task: # Are we creating a new Linked Device? 77 | 78 | if self._task.done(): 79 | 80 | return self.async_show_progress_done( 81 | next_step_id="complete_new_entry" 82 | ) 83 | 84 | _LOGGER.debug("Link task not done. Keep spinning") 85 | # Task not done, keep waiting 86 | return await self.async_step_create_linked_device( 87 | user_input=user_input, 88 | ) 89 | 90 | # Prepare form: Make a dict of all currently linked phones + tokens 91 | self.current_linked_devices = {} 92 | for entry in self._async_current_entries(): 93 | if entry.domain == PALGATE_DOMAIN: 94 | self.current_linked_devices[entry.data[CONF_PHONE_NUMBER]] = \ 95 | [entry.data[CONF_TOKEN], entry.data[CONF_TOKEN_TYPE]] 96 | 97 | return self.async_show_form( 98 | step_id="user", 99 | data_schema=self._create_schema(), 100 | errors={}, 101 | ) 102 | 103 | async def async_step_complete_new_entry( 104 | self, user_input: dict[str, Any] | None = None 105 | ) -> FlowResult: 106 | 107 | if self._task: 108 | if _exc := self._task.exception(): 109 | 110 | _LOGGER.error(_exc) 111 | return self.async_abort(reason=_exc.args[0]) 112 | 113 | self.user_input[CONF_PHONE_NUMBER] = self._linked_phone_number 114 | self.user_input[CONF_TOKEN] = self._linked_token 115 | self.user_input[CONF_TOKEN_TYPE] = self._linked_token_type 116 | 117 | return self.async_create_entry( 118 | title=self.user_input[CONF_DEVICE_ID].title(), 119 | data=self.user_input, 120 | ) 121 | 122 | async def async_step_create_linked_device( 123 | self, user_input: dict[str, Any] | None = None 124 | ) -> FlowResult: 125 | 126 | if not self.linking_code: 127 | self.linking_code = str(uuid.uuid4()) 128 | 129 | # Generate a QR code for Palgate Device Linking 130 | self._qr_img_str = pyqrcode.create( 131 | f'{{"id": "{self.linking_code}"}}', 132 | error='L' 133 | ).png_as_base64_str( 134 | scale=15, 135 | quiet_zone=4 136 | ) 137 | 138 | # Fire off a backend task that waits for vendor API confirmation 139 | # of user scanning the QR code 140 | self._task = self.hass.async_create_task( 141 | self._wait_device_linking() 142 | ) 143 | 144 | # Show form with progress spinner until linking completes 145 | return self.async_show_progress( 146 | progress_action="wait_for_qr", 147 | progress_task=self._task, 148 | description_placeholders={ 149 | "qr_code": 150 | f"" 151 | }, 152 | ) 153 | 154 | # Wait for linking confirmation and token from vendor. Runs as asyncio.task 155 | async def _wait_device_linking(self): 156 | 157 | _session = async_get_clientsession(self.hass) 158 | 159 | _url = 'https://api1.pal-es.com/v1/bt/un/secondary/init/' +\ 160 | f'{self.linking_code}' 161 | 162 | # Work around an apparent HASS quirk, - when show_progress is called 163 | # and the task is already done (e.g. exception), show_progress 164 | # spins forever. Wait a sec to let show_progress kick in before us :-( 165 | await asyncio.sleep(1) 166 | 167 | async with _session.get( 168 | url=_url 169 | ) as resp: 170 | 171 | if resp.status == HTTPStatus.UNAUTHORIZED: 172 | raise Exception(f"Unauthorized. {resp.status}") 173 | 174 | if resp.status != HTTPStatus.OK: 175 | raise Exception(f"Not OK {resp.status} {await resp.text()}") 176 | 177 | try: 178 | _response = json.loads(_text := await resp.text()) 179 | 180 | except json.JSONDecodeError: 181 | _LOGGER.error("Link new dev, vendor response not valid JSON:") 182 | _LOGGER.error(_text) 183 | raise HomeAssistantError("Response from vendor not valid JSON") 184 | 185 | self._linked_phone_number = _response["user"]["id"] 186 | self._linked_token = _response["user"]["token"] 187 | self._linked_token_type = _response["secondary"] 188 | self._linked_status = _response["status"] 189 | 190 | async def async_step_reconfigure( 191 | self, user_input: dict[str, Any] | None = None 192 | ) -> FlowResult: 193 | """Handle a flow initialized by the user.""" 194 | 195 | if user_input is not None: 196 | 197 | if user_input[CONF_DEVICE_ID] != self.device_id: 198 | return self.async_show_form( 199 | step_id="reconfigure", 200 | data_schema=self._create_schema(), 201 | errors={"base":"cant_reconfigure_device_id"}, 202 | ) 203 | 204 | return self.async_update_reload_and_abort( 205 | self._get_reconfigure_entry(), 206 | data_updates=user_input, 207 | ) 208 | self.entry = self._get_reconfigure_entry() 209 | self.device_id = self.entry.data[CONF_DEVICE_ID] 210 | 211 | return self.async_show_form( 212 | step_id="reconfigure", 213 | data_schema=self._create_schema(), 214 | errors={}, 215 | ) 216 | 217 | async def _async_existing_devices(self, area: str) -> bool: 218 | """Find existing devices.""" 219 | 220 | existing_devices = [ 221 | f"{entry.data.get(CONF_DEVICE_ID)}" 222 | for entry in self._async_current_entries() 223 | ] 224 | 225 | return area in existing_devices 226 | 227 | def _create_schema(self) -> vol.Schema: 228 | 229 | def_device_id = self.entry.data[CONF_DEVICE_ID] \ 230 | if self.source == config_entries.SOURCE_RECONFIGURE \ 231 | else None 232 | def_token = self.entry.data[CONF_TOKEN] \ 233 | if self.source == config_entries.SOURCE_RECONFIGURE \ 234 | else None 235 | def_phone = self.entry.data[CONF_PHONE_NUMBER] \ 236 | if self.source == config_entries.SOURCE_RECONFIGURE \ 237 | else None 238 | def_token_type = self.entry.data[CONF_TOKEN_TYPE] \ 239 | if self.source == config_entries.SOURCE_RECONFIGURE \ 240 | else "1" 241 | def_sec_to_open = \ 242 | self.entry.data[CONF_ADVANCED][CONF_SECONDS_TO_OPEN] \ 243 | if self.source == config_entries.SOURCE_RECONFIGURE \ 244 | else SECONDS_TO_OPEN 245 | def_sec_open = \ 246 | self.entry.data[CONF_ADVANCED][CONF_SECONDS_OPEN] \ 247 | if self.source == config_entries.SOURCE_RECONFIGURE \ 248 | else SECONDS_OPEN 249 | def_sec_to_close = \ 250 | self.entry.data[CONF_ADVANCED][CONF_SECONDS_TO_CLOSE] \ 251 | if self.source == config_entries.SOURCE_RECONFIGURE \ 252 | else SECONDS_TO_CLOSE 253 | def_allow_invert = \ 254 | self.entry.data[CONF_ADVANCED][CONF_ALLOW_INVERT_AS_STOP] \ 255 | if self.source == config_entries.SOURCE_RECONFIGURE \ 256 | else False 257 | 258 | _schema = vol.Schema( 259 | { 260 | vol.Required(CONF_DEVICE_ID, default=def_device_id): str, 261 | }) 262 | 263 | if not self.source == config_entries.SOURCE_RECONFIGURE: 264 | _schema = _schema.extend( 265 | { 266 | vol.Required(CONF_PHONE_NUMBER): selector({ 267 | "select": { 268 | "mode": "dropdown", 269 | "options": list(self.current_linked_devices.keys()) + 270 | [ CONF_LINK_NEW_DEVICE ], 271 | } 272 | }) 273 | }) 274 | 275 | _schema = _schema.extend( 276 | { 277 | vol.Required(CONF_ADVANCED): section( 278 | vol.Schema( 279 | { 280 | vol.Required(CONF_SECONDS_TO_OPEN, 281 | default=def_sec_to_open): int, 282 | vol.Required(CONF_SECONDS_OPEN, 283 | default=def_sec_open): int, 284 | vol.Required(CONF_SECONDS_TO_CLOSE, 285 | default=def_sec_to_close): int, 286 | vol.Required(CONF_ALLOW_INVERT_AS_STOP, 287 | default=def_allow_invert): bool, 288 | } 289 | ), 290 | {"collapsed": False \ 291 | if self.source == config_entries.SOURCE_RECONFIGURE 292 | else True 293 | } 294 | ) 295 | }) 296 | 297 | return _schema 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------