├── .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 | 
4 | [](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 | [](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 |
--------------------------------------------------------------------------------