├── requirements_dev.txt ├── custom_components ├── __init__.py └── flichub │ ├── manifest.json │ ├── const.py │ ├── translations │ └── en.json │ ├── strings.json │ ├── sensor.py │ ├── entity.py │ ├── __init__.py │ ├── config_flow.py │ └── binary_sensor.py ├── hacs.json ├── .github ├── workflows │ ├── release-drafter.yaml │ ├── cron.yaml │ ├── release.yaml │ └── validate.yaml └── release-drafter.yml ├── manage └── update_manifest.py ├── LICENSE ├── setup.cfg ├── README.md └── .gitignore /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | homeassistant 2 | pyflichub-tcpclient -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | """Dummy init so that pytest works.""" 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flic Hub", 3 | "hacs": "1.6.0", 4 | "homeassistant": "2021.12.0", 5 | "hide_default_branch": true, 6 | "zip_release": true, 7 | "filename": "flichub.zip" 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | name: Update release draft 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Create Release 14 | uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | 19 | -------------------------------------------------------------------------------- /custom_components/flichub/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "flichub", 3 | "name": "Flic Hub", 4 | "codeowners": ["@JohNan"], 5 | "config_flow": true, 6 | "dhcp": [ 7 | { 8 | "hostname": "flichub", 9 | "macaddress": "76EE2A*" 10 | }, 11 | { 12 | "hostname": "flichub", 13 | "macaddress": "36B5AD*" 14 | } 15 | ], 16 | "documentation": "https://github.com/JohNan/home-assistant-flichub", 17 | "iot_class": "local_push", 18 | "issue_tracker": "https://github.com/JohNan/home-assistant-flichub/issues", 19 | "loggers": ["pyflichub"], 20 | "requirements": ["pyflichub-tcpclient==0.1.10"], 21 | "version": "v0.0.0" 22 | } 23 | -------------------------------------------------------------------------------- /custom_components/flichub/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Flic Hub.""" 2 | from homeassistant.const import Platform 3 | 4 | # Base component constants 5 | NAME = "Flic Hub" 6 | DOMAIN = "flichub" 7 | DOMAIN_DATA = f"{DOMAIN}_data" 8 | VERSION = "0.0.0" 9 | REQUIRED_SERVER_VERSION = "0.1.8" 10 | DEFAULT_SCAN_INTERVAL = 60 11 | 12 | CLIENT_READY_TIMEOUT = 20.0 13 | 14 | # Icons 15 | ICON = "mdi:format-quote-close" 16 | 17 | DATA_BUTTONS = "buttons" 18 | DATA_HUB = "network" 19 | 20 | # Device classes 21 | BINARY_SENSOR_DEVICE_CLASS = "connectivity" 22 | 23 | # Events 24 | EVENT_CLICK = f"{DOMAIN}_click" 25 | EVENT_DATA_CLICK_TYPE = "click_type" 26 | EVENT_DATA_NAME = "name" 27 | EVENT_DATA_SERIAL_NUMBER = "serial_number" 28 | 29 | # Platforms 30 | PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] 31 | 32 | # Defaults 33 | DEFAULT_NAME = DOMAIN -------------------------------------------------------------------------------- /manage/update_manifest.py: -------------------------------------------------------------------------------- 1 | """Update the manifest file.""" 2 | import sys 3 | import json 4 | import os 5 | 6 | 7 | def update_manifest(): 8 | """Update the manifest file.""" 9 | version = "0.0.0" 10 | for index, value in enumerate(sys.argv): 11 | if value in ["--version", "-V"]: 12 | version = sys.argv[index + 1] 13 | 14 | with open(f"{os.getcwd()}/custom_components/flichub/manifest.json") as manifestfile: 15 | manifest = json.load(manifestfile) 16 | 17 | manifest["version"] = version 18 | 19 | with open( 20 | f"{os.getcwd()}/custom_components/flichub/manifest.json", "w" 21 | ) as manifestfile: 22 | manifestfile.write(json.dumps(manifest, indent=4, sort_keys=False)) 23 | 24 | # print output 25 | print("# generated manifest.json") 26 | for key, value in manifest.items(): 27 | print(f"{key}: {value}") 28 | 29 | update_manifest() -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | change-template: '- #$NUMBER $TITLE @$AUTHOR' 4 | sort-direction: ascending 5 | categories: 6 | - title: '🚀 Features' 7 | labels: 8 | - 'feature' 9 | - 'enhancement' 10 | 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | 17 | - title: '🧰 Maintenance' 18 | label: 'chore' 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'major' 24 | minor: 25 | labels: 26 | - 'minor' 27 | patch: 28 | labels: 29 | - 'patch' 30 | default: patch 31 | template: | 32 | [![Downloads for this release](https://img.shields.io/github/downloads/JohNan/home-assistant-flichub/v$RESOLVED_VERSION/total.svg)](https://github.com/JohNan/home-assistant-flichub/releases/v$RESOLVED_VERSION) 33 | ## Changes 34 | 35 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/cron.yaml: -------------------------------------------------------------------------------- 1 | name: "Cron actions 16:30 every 4 days" 2 | on: 3 | schedule: 4 | - cron: '30 16 */4 * *' 5 | 6 | jobs: 7 | validate-hassfest: 8 | name: With hassfest 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v3 13 | 14 | - name: "Update manifest.json" 15 | run: | 16 | python3 ${{ github.workspace }}/manage/update_manifest.py 17 | 18 | - name: Hassfest validation 19 | uses: home-assistant/actions/hassfest@master 20 | 21 | validate-hacs: 22 | name: With HACS action 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Check out repository 26 | uses: actions/checkout@v3 27 | 28 | - name: "Update manifest.json" 29 | run: | 30 | python3 ${{ github.workspace }}/manage/update_manifest.py 31 | 32 | - name: HACS Validation 33 | uses: hacs/action@main 34 | with: 35 | ignore: brands 36 | category: integration 37 | comment: False 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release_zip_file: 9 | name: Prepare release asset 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Get version 16 | id: version 17 | uses: home-assistant/actions/helpers/version@master 18 | 19 | - name: "Set version number" 20 | run: | 21 | python3 ${{ github.workspace }}/manage/update_manifest.py --version ${{ steps.version.outputs.version }} 22 | 23 | - name: Create zip 24 | run: | 25 | cd custom_components/flichub 26 | zip flichub.zip -r ./ 27 | 28 | - name: Upload zip to release 29 | uses: svenstaro/upload-release-action@v1-release 30 | with: 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | file: ./custom_components/flichub/flichub.zip 33 | asset_name: flichub.zip 34 | tag: ${{ github.ref }} 35 | overwrite: true -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: "Validate" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | validate-hassfest: 13 | name: With hassfest 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v3 18 | 19 | - name: "Update manifest.json" 20 | run: | 21 | python3 ${{ github.workspace }}/manage/update_manifest.py 22 | 23 | - name: Hassfest validation 24 | uses: home-assistant/actions/hassfest@master 25 | 26 | validate-hacs: 27 | name: With HACS action 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check out repository 31 | uses: actions/checkout@v3 32 | 33 | - name: "Update manifest.json" 34 | run: | 35 | python3 ${{ github.workspace }}/manage/update_manifest.py 36 | 37 | - name: HACS Validation 38 | uses: hacs/action@main 39 | with: 40 | ignore: brands 41 | category: integration 42 | comment: True 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JohNan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /custom_components/flichub/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Flic Hub", 6 | "description": "If you need help with the configuration have a look here: https://github.com/JohNan/home-assistant-flichub. Default port is 8124", 7 | "data": { 8 | "name": "Name your device", 9 | "ip_address": "IP Address", 10 | "port": "Port" 11 | } 12 | } 13 | }, 14 | "error": { 15 | "cannot_connect": "Unable to connect to device" 16 | }, 17 | "abort": { 18 | "single_instance_allowed": "Only a single instance is allowed.", 19 | "already_configured": "Device is already configured" 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "user": { 25 | "data": { 26 | "binary_sensor": "Binary sensor enabled", 27 | "sensor": "Sensor enabled" 28 | } 29 | } 30 | } 31 | }, 32 | "issues": { 33 | "flichub_invalid_server_version": { 34 | "title": "Invalid server version on FlicHub", 35 | "description": "The TCP server on the FlicHub is running version `{flichub_version}` which does not match the required version `{required_version}`. Please go to https://github.com/JohNan/pyflichub-tcpclient and follow the instructions" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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.flichub, tests 35 | combine_as_imports = true 36 | 37 | [tool:pytest] 38 | addopts = -qq --cov=custom_components.flichub 39 | console_output_style = count 40 | 41 | [coverage:run] 42 | branch = False 43 | 44 | [coverage:report] 45 | show_missing = true 46 | fail_under = 100 47 | -------------------------------------------------------------------------------- /custom_components/flichub/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Flic Hub", 6 | "description": "If you need help with the configuration have a look here: https://github.com/JohNan/home-assistant-flichub. Default port is 8124", 7 | "data": { 8 | "name": "Name your device", 9 | "ip_address": "[%key:common::config_flow::data::ip%]", 10 | "port": "[%key:common::config_flow::data::port%]" 11 | } 12 | } 13 | }, 14 | "error": { 15 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" 16 | }, 17 | "abort": { 18 | "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", 19 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "user": { 25 | "data": { 26 | "binary_sensor": "Binary sensor enabled", 27 | "sensor": "Sensor enabled" 28 | } 29 | } 30 | } 31 | }, 32 | "issues": { 33 | "flichub_invalid_server_version": { 34 | "title": "Invalid server version on FlicHub", 35 | "description": "The TCP server on the FlicHub is running version `{flichub_version}` which does not match the required version `{required_version}`. Please go to https://github.com/JohNan/pyflichub-tcpclient and follow the instructions" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flic Hub 2 | 3 | [![GitHub Release][releases-shield]][releases] 4 | [![License][license-shield]](LICENSE) 5 | 6 | [![Project Maintenance][maintenance-shield]][user_profile] 7 | [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] 8 | 9 | ## Prerequisites 10 | 11 | Add the tcp client to Flic Hub found in this repo: https://github.com/JohNan/pyflichub-tcpclient 12 | 13 | ### Install with HACS (recommended) 14 | Add the url to the repository as a custom integration. 15 | 16 | ## Installation 17 | 18 | 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). 19 | 2. If you do not have a `custom_components` directory (folder) there, you need to create it. 20 | 3. In the `custom_components` directory (folder) create a new folder called `flichub`. 21 | 4. Download _all_ the files from the `custom_components/flichub/` directory (folder) in this repository. 22 | 5. Place the files you downloaded in the new directory (folder) you created. 23 | 6. Restart Home Assistant 24 | 7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Flic Hub" 25 | 26 | ### DHCP Discovery 27 | Your FlicHub should automatically be discovered as a new integration based on dhcp discovery. 28 | If that doesn't work it can be setup manually by doing step 7 in the installation instructions 29 | 30 | [buymecoffee]: https://www.buymeacoffee.com/JohNan 31 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge 32 | [license-shield]: https://img.shields.io/github/license/JohNan/home-assistant-flichub.svg?style=for-the-badge 33 | [maintenance-shield]: https://img.shields.io/badge/maintainer-%40JohNan-blue.svg?style=for-the-badge 34 | [releases-shield]: https://img.shields.io/github/release/JohNan/home-assistant-flichub.svg?style=for-the-badge 35 | [releases]: https://github.com/JohNan/home-assistant-flichub/releases 36 | [user_profile]: https://github.com/JohNan -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hide some OS X stuff 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | 7 | # Thumbnails 8 | ._* 9 | 10 | # IntelliJ IDEA 11 | .idea 12 | *.iml 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ -------------------------------------------------------------------------------- /custom_components/flichub/sensor.py: -------------------------------------------------------------------------------- 1 | """Binary sensor platform for Flic Hub.""" 2 | import logging 3 | from homeassistant.util.dt import get_time_zone, as_utc 4 | 5 | from homeassistant.helpers.device_registry import format_mac 6 | 7 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 8 | 9 | from homeassistant.const import CONF_IP_ADDRESS, EntityCategory, CONF_NAME, PERCENTAGE 10 | 11 | from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT 12 | from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass 13 | from pyflichub.button import FlicButton 14 | from pyflichub.flichub import FlicHubInfo 15 | from . import FlicHubEntryData 16 | from .const import DEFAULT_NAME, DATA_BUTTONS, DATA_HUB 17 | from .const import DOMAIN 18 | from .entity import FlicHubButtonEntity, FlicHubEntity 19 | 20 | _LOGGER: logging.Logger = logging.getLogger(__package__) 21 | 22 | 23 | async def async_setup_entry(hass, entry, async_add_devices): 24 | """Setup binary_sensor platform.""" 25 | data_entry: FlicHubEntryData = hass.data[DOMAIN][entry.entry_id] 26 | buttons = data_entry.coordinator.data[DATA_BUTTONS] 27 | flic_hub = data_entry.coordinator.data[DATA_HUB] 28 | devices = [] 29 | for serial_number, button in buttons.items(): 30 | devices.append(FlicHubButtonBatterySensor(data_entry.coordinator, entry, button, flic_hub)) 31 | devices.append(FlicHubButtonBatteryTimestampSensor(data_entry.coordinator, entry, button, flic_hub)) 32 | async_add_devices(devices) 33 | 34 | 35 | class FlicHubButtonBatterySensor(FlicHubButtonEntity, SensorEntity): 36 | """flichub binary_sensor class.""" 37 | _attr_native_unit_of_measurement = PERCENTAGE 38 | _attr_device_class = SensorDeviceClass.BATTERY 39 | _attr_entity_category = EntityCategory.DIAGNOSTIC 40 | _attr_state_class = SensorStateClass.MEASUREMENT 41 | 42 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 43 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 44 | 45 | @property 46 | def native_value(self): 47 | """Return the state of the sensor.""" 48 | return self.button.battery_status 49 | 50 | @property 51 | def unique_id(self): 52 | """Return a unique ID to use for this entity.""" 53 | return f"{self.serial_number}-battery" 54 | 55 | 56 | class FlicHubButtonBatteryTimestampSensor(FlicHubButtonEntity, SensorEntity): 57 | """flichub binary_sensor class.""" 58 | _attr_device_class = SensorDeviceClass.TIMESTAMP 59 | _attr_entity_category = EntityCategory.DIAGNOSTIC 60 | 61 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 62 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 63 | 64 | @property 65 | def native_value(self): 66 | """Return the state of the sensor.""" 67 | return as_utc(self.button.battery_timestamp) if self.button.battery_timestamp else None 68 | 69 | @property 70 | def unique_id(self): 71 | """Return a unique ID to use for this entity.""" 72 | return f"{self.serial_number}-battery-timestamp" 73 | 74 | @property 75 | def available(self) -> bool: 76 | """Return True if entity is available.""" 77 | return True 78 | -------------------------------------------------------------------------------- /custom_components/flichub/entity.py: -------------------------------------------------------------------------------- 1 | """FlicHubEntity class""" 2 | from typing import Mapping, Any 3 | 4 | from homeassistant.const import CONF_IP_ADDRESS 5 | 6 | from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, CONNECTION_NETWORK_MAC, format_mac 7 | 8 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 9 | from pyflichub.button import FlicButton 10 | from pyflichub.flichub import FlicHubInfo 11 | 12 | from .const import DOMAIN, DATA_BUTTONS, DATA_HUB 13 | 14 | 15 | class FlicHubButtonEntity(CoordinatorEntity): 16 | _attr_has_entity_name = True 17 | 18 | def __init__(self, coordinator, config_entry, serial_number, flic_hub: FlicHubInfo): 19 | super().__init__(coordinator) 20 | self.coordinator = coordinator 21 | self.serial_number = serial_number 22 | self.config_entry = config_entry 23 | self.flic_hub = flic_hub 24 | 25 | @property 26 | def hub_mac_address(self): 27 | """Return a unique ID to use for this entity.""" 28 | if self.flic_hub.has_ethernet(): 29 | return format_mac(self.flic_hub.ethernet.mac) 30 | if self.flic_hub.has_wifi(): 31 | return format_mac(self.flic_hub.wifi.mac) 32 | 33 | @property 34 | def mac_address(self): 35 | return format_mac(self.button.bdaddr) 36 | 37 | @property 38 | def device_info(self): 39 | return { 40 | "identifiers": {(DOMAIN, self.serial_number)}, 41 | "name": self.button.name, 42 | "model": self.button.flic_version, 43 | "connections": {(CONNECTION_BLUETOOTH, self.mac_address)}, 44 | "sw_version": self.button.firmware_version, 45 | "hw_version": self.button.flic_version, 46 | "manufacturer": "Flic", 47 | "via_device": (DOMAIN, self.hub_mac_address) 48 | } 49 | 50 | @property 51 | def button(self) -> FlicButton: 52 | return self.coordinator.data[DATA_BUTTONS][self.serial_number] 53 | 54 | @property 55 | def extra_state_attributes(self) -> Mapping[str, Any] | None: 56 | """Return the state attributes.""" 57 | return { 58 | "color": self.button.color, 59 | "bluetooth_address": self.button.bdaddr, 60 | "serial_number": self.button.serial_number, 61 | "integration": DOMAIN, 62 | } 63 | 64 | @property 65 | def available(self) -> bool: 66 | """Return True if entity is available.""" 67 | return self.button.connected 68 | 69 | 70 | class FlicHubEntity(CoordinatorEntity): 71 | _attr_has_entity_name = True 72 | 73 | def __init__(self, coordinator, config_entry, flic_hub: FlicHubInfo): 74 | super().__init__(coordinator) 75 | self.coordinator = coordinator 76 | self._flic_hub = flic_hub 77 | self._ip_address = config_entry.data[CONF_IP_ADDRESS] 78 | self.config_entry = config_entry 79 | 80 | @property 81 | def mac_address(self): 82 | """Return a unique ID to use for this entity.""" 83 | if self.flic_hub.has_ethernet() and self._ip_address == self.flic_hub.ethernet.ip: 84 | return format_mac(self.flic_hub.ethernet.mac) 85 | if self.flic_hub.has_wifi() and self._ip_address == self.flic_hub.wifi.ip: 86 | return format_mac(self.flic_hub.wifi.mac) 87 | 88 | @property 89 | def device_info(self): 90 | identifiers = set() 91 | connections = set() 92 | 93 | if self.flic_hub.has_ethernet() and self._ip_address == self.flic_hub.ethernet.ip: 94 | identifiers.add((DOMAIN, format_mac(self.flic_hub.ethernet.mac))) 95 | connections.add((DOMAIN, format_mac(self.flic_hub.ethernet.mac))) 96 | if self.flic_hub.has_wifi() and self._ip_address == self.flic_hub.wifi.ip: 97 | identifiers.add((DOMAIN, format_mac(self.flic_hub.wifi.mac))) 98 | connections.add((DOMAIN, format_mac(self.flic_hub.wifi.mac))) 99 | 100 | return { 101 | "identifiers": identifiers, 102 | "name": "FlicHub", 103 | "model": "LR", 104 | "connections": connections, 105 | "manufacturer": "Flic" 106 | } 107 | 108 | @property 109 | def flic_hub(self) -> FlicHubInfo: 110 | if DATA_HUB not in self.coordinator.data: 111 | return self._flic_hub 112 | else: 113 | return self.coordinator.data[DATA_HUB] 114 | -------------------------------------------------------------------------------- /custom_components/flichub/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom integration to integrate Flic Hub with Home Assistant. 3 | 4 | For more details about this integration, please refer to 5 | https://github.com/JohNan/flichub 6 | """ 7 | import async_timeout 8 | import asyncio 9 | import logging 10 | from dataclasses import dataclass 11 | from datetime import timedelta 12 | from homeassistant.helpers.issue_registry import async_create_issue, IssueSeverity 13 | 14 | from homeassistant.config_entries import ConfigEntry 15 | from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP 16 | from homeassistant.core import HomeAssistant 17 | from homeassistant.exceptions import ConfigEntryNotReady 18 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 19 | from pyflichub.button import FlicButton 20 | from pyflichub.client import FlicHubTcpClient, ServerCommand 21 | from pyflichub.command import Command 22 | from pyflichub.event import Event 23 | from .const import CLIENT_READY_TIMEOUT, EVENT_CLICK, EVENT_DATA_NAME, EVENT_DATA_CLICK_TYPE, \ 24 | EVENT_DATA_SERIAL_NUMBER, DATA_BUTTONS, DATA_HUB, REQUIRED_SERVER_VERSION, DEFAULT_SCAN_INTERVAL 25 | from .const import DOMAIN 26 | from .const import PLATFORMS 27 | 28 | SCAN_INTERVAL = timedelta(seconds=30) 29 | 30 | _LOGGER: logging.Logger = logging.getLogger(__package__) 31 | 32 | 33 | @dataclass 34 | class FlicHubEntryData: 35 | """Class for sharing data within the Nanoleaf integration.""" 36 | 37 | client: FlicHubTcpClient 38 | coordinator: DataUpdateCoordinator[dict] 39 | 40 | 41 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 42 | """Set up this integration using UI.""" 43 | if hass.data.get(DOMAIN) is None: 44 | hass.data.setdefault(DOMAIN, {}) 45 | 46 | def on_event(button: FlicButton, event: Event): 47 | _LOGGER.debug(f"Event: {event}") 48 | if event.event == "button": 49 | hass.bus.fire(EVENT_CLICK, { 50 | EVENT_DATA_SERIAL_NUMBER: button.serial_number, 51 | EVENT_DATA_NAME: button.name, 52 | EVENT_DATA_CLICK_TYPE: event.action 53 | }) 54 | if event.event == "buttonReady": 55 | hass.async_create_task(coordinator.async_refresh()) 56 | 57 | def on_command(command: Command): 58 | _LOGGER.debug(f"Command: {command.command}, data: {command.data}") 59 | if command is None: 60 | return 61 | if command.command == ServerCommand.SERVER_INFO: 62 | hub_version = command.data.version 63 | if hub_version != REQUIRED_SERVER_VERSION: 64 | async_create_issue( 65 | hass, 66 | DOMAIN, 67 | f"invalid_server_version_{entry.entry_id}", 68 | is_fixable=False, 69 | severity=IssueSeverity.ERROR, 70 | translation_key=f"{DOMAIN}_invalid_server_version", 71 | translation_placeholders={ 72 | "required_version": REQUIRED_SERVER_VERSION, 73 | "flichub_version": hub_version, 74 | }, 75 | ) 76 | if command.command == ServerCommand.BUTTONS: 77 | coordinator.async_set_updated_data( 78 | { 79 | DATA_BUTTONS: {button.serial_number: button for button in command.data}, 80 | DATA_HUB: coordinator.data.get(DATA_HUB, None) if coordinator.data else None 81 | } 82 | ) 83 | 84 | client = FlicHubTcpClient( 85 | ip=entry.data[CONF_IP_ADDRESS], 86 | port=entry.data[CONF_PORT], 87 | loop=asyncio.get_event_loop(), 88 | event_callback=on_event, 89 | command_callback=on_command 90 | ) 91 | client_ready = asyncio.Event() 92 | 93 | async def async_update() -> dict: 94 | buttons = await client.get_buttons() 95 | hub_info = await client.get_hubinfo() 96 | return { 97 | DATA_BUTTONS: {button.serial_number: button for button in buttons} if buttons is not None else {}, 98 | DATA_HUB: hub_info if hub_info is not None else {} 99 | } 100 | 101 | async def client_connected(): 102 | _LOGGER.debug("Connected!") 103 | client_ready.set() 104 | await client.get_server_info() 105 | 106 | async def client_disconnected(): 107 | _LOGGER.debug("Disconnected!") 108 | 109 | def stop_client(event): 110 | client.disconnect() 111 | 112 | client.async_on_connected = client_connected 113 | client.async_on_disconnected = client_disconnected 114 | 115 | await client.async_connect() 116 | 117 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) 118 | 119 | try: 120 | with async_timeout.timeout(CLIENT_READY_TIMEOUT): 121 | await client_ready.wait() 122 | except asyncio.TimeoutError: 123 | _LOGGER.error(f"Client not connected after {CLIENT_READY_TIMEOUT} secs. Discontinuing setup") 124 | client.disconnect() 125 | raise ConfigEntryNotReady 126 | 127 | coordinator = DataUpdateCoordinator( 128 | hass, 129 | _LOGGER, 130 | name=entry.title, 131 | update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), 132 | update_method=async_update 133 | ) 134 | 135 | await coordinator.async_config_entry_first_refresh() 136 | 137 | hass.data[DOMAIN][entry.entry_id] = FlicHubEntryData( 138 | client=client, 139 | coordinator=coordinator 140 | ) 141 | 142 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 143 | 144 | entry.add_update_listener(async_reload_entry) 145 | return True 146 | 147 | 148 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 149 | """Handle removal of an entry.""" 150 | hass.data[DOMAIN][entry.entry_id].client.disconnect() 151 | unloaded = all( 152 | await asyncio.gather( 153 | *[ 154 | hass.config_entries.async_forward_entry_unload(entry, platform) 155 | for platform in PLATFORMS 156 | ] 157 | ) 158 | ) 159 | if unloaded: 160 | hass.data[DOMAIN].pop(entry.entry_id) 161 | 162 | return unloaded 163 | 164 | 165 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 166 | """Reload config entry.""" 167 | await async_unload_entry(hass, entry) 168 | await async_setup_entry(hass, entry) 169 | -------------------------------------------------------------------------------- /custom_components/flichub/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for Flic Hub.""" 2 | from homeassistant.data_entry_flow import FlowResult 3 | from typing import Any 4 | 5 | import async_timeout 6 | import asyncio 7 | import logging 8 | import voluptuous as vol 9 | 10 | from homeassistant import config_entries 11 | from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_NAME 12 | from homeassistant.core import callback 13 | from homeassistant.helpers.device_registry import format_mac 14 | from pyflichub.client import FlicHubTcpClient 15 | from .const import CLIENT_READY_TIMEOUT 16 | from .const import DOMAIN 17 | from .const import PLATFORMS 18 | 19 | _LOGGER: logging.Logger = logging.getLogger(__package__) 20 | 21 | 22 | class FlicHubFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 23 | """Config flow for flichub.""" 24 | 25 | VERSION = 1 26 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 27 | 28 | def __init__(self): 29 | """Initialize.""" 30 | self._ip_address = None 31 | self._mac_address = None 32 | self._errors = {} 33 | 34 | async def async_step_dhcp(self, discovery_info) -> FlowResult: 35 | """Handle dhcp discovery.""" 36 | self._ip_address = discovery_info.ip 37 | self._mac_address = discovery_info.macaddress 38 | 39 | self._async_abort_entries_match({CONF_IP_ADDRESS: self._ip_address}) 40 | await self.async_set_unique_id(format_mac(self._mac_address)) 41 | self._abort_if_unique_id_configured( 42 | updates={ 43 | CONF_IP_ADDRESS: self._ip_address 44 | } 45 | ) 46 | 47 | self.context["title_placeholders"] = {CONF_IP_ADDRESS: self._ip_address} 48 | return await self.async_step_user() 49 | 50 | async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: 51 | """Handle a flow initialized by the user.""" 52 | self._errors = {} 53 | 54 | if user_input is not None: 55 | valid, mac = await self._test_credentials( 56 | user_input[CONF_IP_ADDRESS], 57 | user_input[CONF_PORT] 58 | ) 59 | if valid: 60 | self._mac_address = mac 61 | return await self._create_entry(user_input) 62 | self._errors["base"] = "cannot_connect" 63 | 64 | return await self._show_config_form(user_input) 65 | 66 | async def _create_entry(self, user_input) -> FlowResult: 67 | existing_entry = await self.async_set_unique_id( 68 | format_mac(self._mac_address) 69 | ) 70 | 71 | self._abort_if_unique_id_configured( 72 | updates={ 73 | CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], 74 | CONF_PORT: user_input[CONF_PORT], 75 | } 76 | ) 77 | 78 | if existing_entry: 79 | self.hass.config_entries.async_update_entry( 80 | existing_entry, data=user_input 81 | ) 82 | await self.hass.config_entries.async_reload(existing_entry.entry_id) 83 | return self.async_abort(reason="reauth_successful") 84 | return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) 85 | 86 | async def _show_config_form(self, user_input: dict[str, Any] | None = None) -> FlowResult: 87 | """Show the configuration form to edit location data.""" 88 | if user_input is None: 89 | return self.async_show_form( 90 | step_id="user", 91 | data_schema=vol.Schema( 92 | { 93 | vol.Required(CONF_NAME): str, 94 | vol.Required(CONF_IP_ADDRESS, default=self._ip_address): str, 95 | vol.Required(CONF_PORT, default="8124"): str 96 | } 97 | ), 98 | errors=self._errors, 99 | ) 100 | 101 | return self.async_show_form( 102 | step_id="user", 103 | data_schema=vol.Schema( 104 | { 105 | vol.Required(CONF_NAME, default=user_input.get(CONF_NAME, None)): str, 106 | vol.Required(CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS, self._ip_address)): str, 107 | vol.Required(CONF_PORT, default=user_input.get(CONF_PORT, "8124")): str 108 | } 109 | ), 110 | errors=self._errors, 111 | ) 112 | 113 | @staticmethod 114 | async def _test_credentials(ip, port) -> tuple[bool, str | None]: 115 | """Return true if credentials is valid.""" 116 | client = FlicHubTcpClient(ip, port, asyncio.get_event_loop()) 117 | try: 118 | client_ready = asyncio.Event() 119 | 120 | async def client_connected(): 121 | client_ready.set() 122 | 123 | async def client_disconnected(): 124 | _LOGGER.debug("Disconnected") 125 | 126 | client.async_on_connected = client_connected 127 | client.async_on_disconnected = client_disconnected 128 | 129 | asyncio.create_task(client.async_connect()) 130 | 131 | with async_timeout.timeout(CLIENT_READY_TIMEOUT): 132 | await client_ready.wait() 133 | 134 | hub_info = await client.get_hubinfo() 135 | if hub_info.has_wifi() and hub_info.wifi.ip == ip: 136 | client.disconnect() 137 | return True, hub_info.wifi.mac 138 | 139 | if hub_info.has_ethernet() and hub_info.ethernet.ip == ip: 140 | client.disconnect() 141 | return True, hub_info.ethernet.mac 142 | 143 | client.disconnect() 144 | return False, None 145 | except Exception as e: # pylint: disable=broad-except 146 | _LOGGER.error("Error connecting", exc_info=e) 147 | client.disconnect() 148 | pass 149 | return False, None 150 | 151 | @staticmethod 152 | @callback 153 | def async_get_options_flow(config_entry): 154 | return FlicHubOptionsFlowHandler(config_entry) 155 | 156 | 157 | class FlicHubOptionsFlowHandler(config_entries.OptionsFlow): 158 | """Config flow options handler for flichub.""" 159 | 160 | def __init__(self, config_entry): 161 | """Initialize HACS options flow.""" 162 | self.config_entry = config_entry 163 | self.options = dict(config_entry.options) 164 | 165 | async def async_step_init(self, user_input=None): # pylint: disable=unused-argument 166 | """Manage the options.""" 167 | return await self.async_step_user() 168 | 169 | async def async_step_user(self, user_input=None): 170 | """Handle a flow initialized by the user.""" 171 | if user_input is not None: 172 | self.options.update(user_input) 173 | return await self._update_options() 174 | 175 | return self.async_show_form( 176 | step_id="user", 177 | data_schema=vol.Schema( 178 | { 179 | vol.Required(x, default=self.options.get(x, True)): bool 180 | for x in sorted(PLATFORMS) 181 | } 182 | ), 183 | ) 184 | 185 | async def _update_options(self): 186 | """Update config entry options.""" 187 | return self.async_create_entry( 188 | title=self.config_entry.data.get(CONF_IP_ADDRESS), data=self.options 189 | ) 190 | -------------------------------------------------------------------------------- /custom_components/flichub/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Binary sensor platform for Flic Hub.""" 2 | 3 | import logging 4 | 5 | from homeassistant import core 6 | from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers.entity import EntityCategory 9 | from pyflichub.button import FlicButton 10 | from pyflichub.event import Event 11 | from pyflichub.flichub import FlicHubInfo 12 | from . import FlicHubEntryData 13 | from .const import DOMAIN 14 | from .const import EVENT_CLICK, EVENT_DATA_CLICK_TYPE, \ 15 | EVENT_DATA_SERIAL_NUMBER, EVENT_DATA_NAME, DATA_BUTTONS, DATA_HUB 16 | from .entity import FlicHubButtonEntity, FlicHubEntity 17 | 18 | _LOGGER: logging.Logger = logging.getLogger(__package__) 19 | 20 | 21 | async def async_setup_entry(hass, entry, async_add_devices): 22 | """Setup binary_sensor platform.""" 23 | data_entry: FlicHubEntryData = hass.data[DOMAIN][entry.entry_id] 24 | buttons = data_entry.coordinator.data[DATA_BUTTONS] 25 | flic_hub = data_entry.coordinator.data[DATA_HUB] 26 | devices = [] 27 | for serial_number, button in buttons.items(): 28 | devices.extend([ 29 | FlicHubButtonBinarySensor(hass, data_entry.coordinator, entry, button, flic_hub), 30 | FlicHubButtonPassiveBinarySensor(data_entry.coordinator, entry, button, flic_hub), 31 | FlicHubButtonActiveDisconnectBinarySensor(data_entry.coordinator, entry, button, flic_hub), 32 | FlicHubButtonConnectedBinarySensor(data_entry.coordinator, entry, button, flic_hub), 33 | FlicHubButtonReadyBinarySensor(data_entry.coordinator, entry, button, flic_hub) 34 | ]) 35 | devices.extend([ 36 | FlicHubWifiBinarySensor(data_entry.coordinator, entry, flic_hub), 37 | FlicHubEthernetBinarySensor(data_entry.coordinator, entry, flic_hub), 38 | ]) 39 | async_add_devices(devices) 40 | 41 | 42 | class FlicHubWifiBinarySensor(FlicHubEntity, BinarySensorEntity): 43 | """flichub sensor class.""" 44 | _attr_entity_category = EntityCategory.DIAGNOSTIC 45 | _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY 46 | _attr_icon = "mdi:wifi" 47 | _attr_name = "Wifi" 48 | 49 | def __init__(self, coordinator, config_entry, flic_hub: FlicHubInfo): 50 | super().__init__(coordinator, config_entry, flic_hub) 51 | self._attr_entity_registry_enabled_default = self.flic_hub.has_wifi() 52 | self._attr_unique_id = f"{self.mac_address}-wifi" 53 | 54 | @property 55 | def is_on(self): 56 | """Return true if the binary_sensor is on.""" 57 | return self.flic_hub.wifi.connected if self.flic_hub.has_wifi() else False 58 | 59 | @property 60 | def extra_state_attributes(self): 61 | """Return the state attributes.""" 62 | return { 63 | "mac_address": self.flic_hub.wifi.mac, 64 | "ip_address": self.flic_hub.wifi.ip, 65 | "state": self.flic_hub.wifi.state, 66 | "ssid": self.flic_hub.wifi.ssid 67 | } 68 | 69 | @property 70 | def available(self) -> bool: 71 | """Return True if entity is available.""" 72 | return self.flic_hub.has_wifi() 73 | 74 | 75 | class FlicHubEthernetBinarySensor(FlicHubEntity, BinarySensorEntity): 76 | """flichub sensor class.""" 77 | _attr_entity_category = EntityCategory.DIAGNOSTIC 78 | _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY 79 | _attr_icon = "mdi:ethernet" 80 | _attr_name = "Ethernet" 81 | 82 | def __init__(self, coordinator, config_entry, flic_hub: FlicHubInfo): 83 | super().__init__(coordinator, config_entry, flic_hub) 84 | self._attr_entity_registry_enabled_default = self.flic_hub.has_ethernet() 85 | self._attr_unique_id = f"{self.mac_address}-ethernet" 86 | 87 | @property 88 | def is_on(self): 89 | """Return true if the binary_sensor is on.""" 90 | return self.flic_hub.ethernet.connected if self.flic_hub.has_ethernet() else False 91 | 92 | @property 93 | def extra_state_attributes(self): 94 | """Return the state attributes.""" 95 | return { 96 | "mac": self.flic_hub.ethernet.mac, 97 | "ip": self.flic_hub.ethernet.ip 98 | } 99 | 100 | @property 101 | def available(self) -> bool: 102 | """Return True if entity is available.""" 103 | return self.flic_hub.has_ethernet() 104 | 105 | 106 | class FlicHubButtonReadyBinarySensor(FlicHubButtonEntity, BinarySensorEntity): 107 | """flichub sensor class.""" 108 | _attr_device_class = BinarySensorDeviceClass.PROBLEM 109 | _attr_entity_category = EntityCategory.DIAGNOSTIC 110 | _attr_name = "Ready" 111 | 112 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 113 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 114 | self._attr_unique_id = f"{self.mac_address}-ready" 115 | 116 | @property 117 | def is_on(self): 118 | """Return true if the binary_sensor is on.""" 119 | return not self.button.ready 120 | 121 | 122 | class FlicHubButtonConnectedBinarySensor(FlicHubButtonEntity, BinarySensorEntity): 123 | """flichub sensor class.""" 124 | _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY 125 | _attr_entity_category = EntityCategory.DIAGNOSTIC 126 | _attr_name = "Connection" 127 | 128 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 129 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 130 | self._attr_unique_id = f"{self.mac_address}-connected" 131 | 132 | @property 133 | def is_on(self): 134 | """Return true if the binary_sensor is on.""" 135 | return self.button.connected 136 | 137 | 138 | class FlicHubButtonPassiveBinarySensor(FlicHubButtonEntity, BinarySensorEntity): 139 | """flichub sensor class.""" 140 | _attr_entity_category = EntityCategory.DIAGNOSTIC 141 | _attr_name = "Passive Mode" 142 | 143 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 144 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 145 | self._attr_unique_id = f"{self.mac_address}-passive_mode" 146 | 147 | @property 148 | def is_on(self): 149 | """Return true if the binary_sensor is on.""" 150 | return self.button.passive_mode 151 | 152 | 153 | class FlicHubButtonActiveDisconnectBinarySensor(FlicHubButtonEntity, BinarySensorEntity): 154 | """flichub sensor class.""" 155 | _attr_entity_category = EntityCategory.DIAGNOSTIC 156 | _attr_name = "Active Disconnect" 157 | 158 | def __init__(self, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 159 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 160 | self._attr_unique_id = f"{self.mac_address}-active_disconnect" 161 | 162 | @property 163 | def is_on(self): 164 | """Return true if the binary_sensor is on.""" 165 | return self.button.active_disconnect 166 | 167 | 168 | class FlicHubButtonBinarySensor(FlicHubButtonEntity, BinarySensorEntity): 169 | """flichub binary_sensor class.""" 170 | _attr_has_entity_name = True 171 | _attr_name = None 172 | _attr_icon = "mdi:hockey-puck" 173 | 174 | def __init__(self, hass: HomeAssistant, coordinator, config_entry, button: FlicButton, flic_hub: FlicHubInfo): 175 | super().__init__(coordinator, config_entry, button.serial_number, flic_hub) 176 | self._attr_unique_id = f"{self.serial_number}-button" 177 | self._is_on = False 178 | self._click_type = None 179 | hass.bus.async_listen(EVENT_CLICK, self._event_callback) 180 | 181 | @property 182 | def is_on(self): 183 | """Return true if the binary_sensor is on.""" 184 | return self._is_on 185 | 186 | @property 187 | def extra_state_attributes(self): 188 | """Return the state attributes.""" 189 | attrs = {"click_type": self._click_type} 190 | attrs.update(super().extra_state_attributes) 191 | return attrs 192 | 193 | def _event_callback(self, event: core.Event): 194 | serial_number = event.data[EVENT_DATA_SERIAL_NUMBER] 195 | """Update the entity.""" 196 | if self.serial_number != serial_number: 197 | return 198 | 199 | name = event.data[EVENT_DATA_NAME] 200 | click_type: Event = event.data[EVENT_DATA_CLICK_TYPE] 201 | _LOGGER.debug(f"Button {name} clicked: {click_type}") 202 | if click_type == 'single': 203 | self._click_type = click_type 204 | elif click_type == 'double': 205 | self._click_type = click_type 206 | 207 | if click_type == 'down': 208 | self._is_on = True 209 | if click_type == 'hold': 210 | self._click_type = click_type 211 | self._is_on = True 212 | if click_type == 'up': 213 | self._is_on = False 214 | self.schedule_update_ha_state() 215 | --------------------------------------------------------------------------------