-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d92955e
Showing
17 changed files
with
615 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* @verdel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
version: 2 | ||
updates: | ||
- package-ecosystem: github-actions | ||
directory: "/" | ||
schedule: | ||
interval: daily | ||
- package-ecosystem: pip | ||
directory: "/" | ||
schedule: | ||
interval: daily |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: HACS validation | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
hacs: | ||
runs-on: "ubuntu-latest" | ||
name: HACS | ||
steps: | ||
- name: Check out the repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: HACS validation | ||
uses: "hacs/[email protected]" | ||
with: | ||
category: "integration" | ||
ignore: brands | ||
|
||
hassfest: | ||
runs-on: "ubuntu-latest" | ||
name: Hassfest | ||
steps: | ||
- name: Check out the repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Hassfest validation | ||
uses: "home-assistant/actions/hassfest@master" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
repos: | ||
- repo: https://github.com/charliermarsh/ruff-pre-commit | ||
rev: "v0.2.2" | ||
hooks: | ||
- id: ruff | ||
args: [--fix, --exit-non-zero-on-fix] | ||
- id: ruff-format |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Home Assistant custom component for Petoneer SmartDot | ||
|
||
This is a custom component for Home Assistant that allows the control of the Petoneer SmartDot via bluetooth. | ||
|
||
![Petoneer SmartDot](assets/petoneer-smartdot.png) | ||
|
||
# Installation | ||
|
||
This custom component can be installed in two different ways: `manually` or `using HACS` | ||
|
||
## 1. Installation using HACS (recommended) | ||
|
||
This repo is now in [HACS](https://hacs.xyz/). | ||
|
||
1. Install HACS follow the instructions [here](https://hacs.xyz/docs/setup/prerequisites) | ||
2. Search for `Petoneer SmartDot` | ||
3. Install and enjoy automatic updates | ||
|
||
## 2. Manual Installation | ||
|
||
1. Download the zip file from the | ||
[latest release](https://github.com/verdel/hass-petoneer-smartdot/releases/latest). | ||
2. Unpack the release and copy the `custom_components/petoneer_smartdot` directory | ||
into the `custom_components` directory of your Home Assistant | ||
installation. | ||
3. Ensure bluez is installed and accessible from HA (refer to next section) | ||
4. Add the `petoneer_smartdot` as described in next section. | ||
|
||
## Ensure Host bluetooth is accessible from Home-Assistant | ||
|
||
Since version 1.0.0, this component uses the [`bleak`](https://github.com/hbldh/bleak) python library to access bluetooth (as bluepy is not supported from HA 2022.07+). In order to scan and interact with bluetooth devices, bluez utility needs to be installed and the correct permissions must be given to HA: | ||
|
||
- for **Home Assistant Operating System**: | ||
It should be all setup, at least for HA 2022.7+ | ||
|
||
- For **Home Assistant Container** in docker: | ||
|
||
Ensure your host has the `bluetoothctl` binary on the system (coming from `bluez` or `bluez-util` package, depending on the distro). | ||
The docker-compose container (or equivalent docker command) should link _/var/run/dbus_ with host folder through a volume and _NET_ADMIN_ permission is needed. docker compose extract: | ||
|
||
```yaml | ||
volumes: | ||
- /run/dbus:/run/dbus:ro | ||
cap_add: | ||
- NET_ADMIN | ||
- NET_RAW | ||
network_mode: host | ||
``` | ||
- For **Home Assistant Core** installed in a Virtualenv: | ||
Ensure your host has the `bluetoothctl` binary on the system (coming from `bluez` or `bluez-util` package, depending on the distro). | ||
Make sure the user running HA belongs to the `bluetooth` group. | ||
|
||
# Homeassistant component configuration | ||
|
||
## Adding the device to HA | ||
|
||
You must have the `bluetooth` integration enabled and configured (HA 2022.8+) or a connected ESPhome device running the bluetooth proxy (HA 2022.10+). The Petoneer SmartDot should be automatically discovered and you will receive a notification prompting you to add it. | ||
|
||
The devices can also be added through the `integration menu` UI: | ||
|
||
- In Configuration/Integrations click on the + button, select `Petoneer SmartDot` and you can either scan for the devices or configure the name and mac address manually on the form. | ||
The SmartDot is automatically added and a device is created. | ||
|
||
Please ensure the following steps prior to adding a new SmartDot: | ||
|
||
- The SmartDot must NOT be connected with the official app (or any other device), else HA will not be able to discover it, nor connect to it. | ||
- Some HA integrations still use some bluetooth libraries that take full control of the physical bluetooth adapter, in that case, other ble integration will not have access to it. So to test this component, best to disable all other ble integrations if you are unsure what ble lib they are using. | ||
|
||
# Debugging | ||
|
||
Please ensure the following: | ||
|
||
1. the petoneer_smartdot integration has been removed from HA. | ||
2. HA has access to the bluetooth adapter (follow the section above in not on HAOS). | ||
3. No other bluetooth integration are using something else than bleak library for bluetooth. If unsure, disable them. | ||
4. The logging has been changed in HA to allow debugging of this component and bleak: | ||
In order to get more information on what is going on, the debugging flag can be enabled by placing in the `configuration.yaml` of Home assistant: | ||
|
||
```yaml | ||
logger: | ||
default: warning | ||
logs: | ||
custom_components.petoneer_smartdot: debug | ||
bleak_retry_connector: debug | ||
bleak: debug | ||
# homeassistant.components.bluetooth: debug # this can help if needed | ||
# homeassistant.components.esphome.bluetooth: debug # this can help if needed | ||
``` | ||
|
||
NOTE: this will generate A LOT of debugging messages in the logs, so it is not recommended to use for a long time | ||
|
||
5. Restart HA | ||
6. Reinstall the petoneer_smartdot integration and find the SmartDot through a scan. | ||
7. check the logs and report. Thanks | ||
|
||
# Other info | ||
|
||
Originally based on the work by Marco Colombo [hass-addon-petoneer-smartdot](https://github.com/marcomow/hass-addon-petoneer-smartdot). |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
"""The Petoneer SmartDot integration.""" | ||
|
||
import logging | ||
|
||
from homeassistant.components.bluetooth import async_ble_device_from_address, async_scanner_count | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_MAC | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
|
||
from .const import DOMAIN, PLATFORMS | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up petoneer_smartdot from a config entry.""" | ||
_LOGGER.debug("integration async setup entry: {entry.as_dict()}") | ||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
address = entry.data.get(CONF_MAC) | ||
|
||
ble_device = async_ble_device_from_address(hass, address.upper(), connectable=True) | ||
_LOGGER.debug("BLE device through HA bt: %s", ble_device) | ||
if ble_device is None: | ||
count_scanners = async_scanner_count(hass, connectable=True) | ||
_LOGGER.debug("Count of BLE scanners in HA bt: %s", count_scanners) | ||
if count_scanners < 1: | ||
raise ConfigEntryNotReady( | ||
"No bluetooth scanner detected. \ | ||
Enable the bluetooth integration or ensure an esphome device \ | ||
is running as a bluetooth proxy" | ||
) | ||
raise ConfigEntryNotReady(f"Could not find Petoneer SmartDot with address {address}") | ||
|
||
hass.data[DOMAIN][entry.entry_id] = {"id": entry.entry_id, "device": ble_device, "mac": address} | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
_LOGGER.debug("async unload entry") | ||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
if unload_ok: | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import asyncio | ||
import logging | ||
|
||
from bleak import BleakClient, BleakError | ||
from bleak_retry_connector import establish_connection | ||
from homeassistant.components.button import ButtonEntity | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.entity_registry import async_entries_for_config_entry, async_get | ||
|
||
from .const import DOMAIN, MODES, UUID_CONTROL_MODE | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def get_select_entity_value(hass, entry_id) -> str | None: | ||
"""Get game mode from select entity value.""" | ||
|
||
entity_registry = async_get(hass) | ||
entries = async_entries_for_config_entry(entity_registry, entry_id) | ||
select_entities = [entry for entry in entries if entry.domain == "select"] | ||
|
||
if select_entities: | ||
select_entity_id = select_entities[0].entity_id | ||
select_entity_state = hass.states.get(select_entity_id) | ||
return select_entity_state.state | ||
|
||
return None | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback): | ||
"""Setup button entities.""" | ||
|
||
data = hass.data[DOMAIN][entry.entry_id] | ||
async_add_entities([SmartDotStartButtonEntity(hass, entry.entry_id, data), SmartDotStopButtonEntity(data)]) | ||
|
||
|
||
class SmartDotStartButtonEntity(ButtonEntity): | ||
"""Petoneer SmartDot button entity for game start.""" | ||
|
||
_attr_should_poll = False | ||
|
||
def __init__(self, hass, entry_id, data) -> None: | ||
"""Initialize the button entity.""" | ||
|
||
self._attr_unique_id = f"{data.get('id')}_start_game" | ||
self._attr_name = "Start" | ||
self._attr_icon = "mdi:play" | ||
self._mac = data.get("mac") | ||
self._ble_device = data.get("device") | ||
self._hass = hass | ||
self._entry_id = entry_id | ||
|
||
_LOGGER.debug("Initializing BLE Device %s (%s)", self._ble_device.name, self._mac) | ||
_LOGGER.debug("BLE_device details: %s", self._ble_device.details) | ||
|
||
async def async_press(self) -> None: | ||
"""Restore last state when added.""" | ||
game_mode = await get_select_entity_value(self._hass, self._entry_id) | ||
if game_mode is None: | ||
_LOGGER.debug("Select game mode first") | ||
return | ||
_LOGGER.debug("Current game mode: %s", game_mode) | ||
_LOGGER.debug("Connecting") | ||
try: | ||
client = await establish_connection( | ||
client_class=BleakClient, device=self._ble_device, name=self._ble_device.address | ||
) | ||
except asyncio.TimeoutError: | ||
_LOGGER.error("Connection Timeout error") | ||
except BleakError as err: | ||
_LOGGER.error("Connection: BleakError: %s", err) | ||
|
||
try: | ||
_LOGGER.debug("Sending command") | ||
await client.write_gatt_char( | ||
UUID_CONTROL_MODE, | ||
bytearray.fromhex(MODES[game_mode]), | ||
response=False, | ||
) | ||
await asyncio.sleep(0.1) | ||
except asyncio.TimeoutError: | ||
_LOGGER.error("Connection Timeout error") | ||
except BleakError as err: | ||
_LOGGER.error("Connection: BleakError: %s", err) | ||
await client.disconnect() | ||
_LOGGER.debug("Disconnected") | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Return the device info.""" | ||
return DeviceInfo( | ||
identifiers={(DOMAIN, self._mac)}, | ||
name=f"Petoneer SmartDot ({self._mac})", | ||
manufacturer="Petoneer", | ||
model="SmartDot", | ||
) | ||
|
||
|
||
class SmartDotStopButtonEntity(ButtonEntity): | ||
"""Petoneer SmartDot button entity for game stop.""" | ||
|
||
_attr_should_poll = False | ||
|
||
def __init__(self, data) -> None: | ||
"""Initialize the button entity.""" | ||
|
||
self._attr_unique_id = f"{data.get('id')}_stop_game" | ||
self._attr_name = "Stop" | ||
self._attr_icon = "mdi:stop" | ||
self._mac = data.get("mac") | ||
self._ble_device = data.get("device") | ||
_LOGGER.debug("Initializing BLE Device %s (%s)", self._ble_device.name, self._mac) | ||
_LOGGER.debug("BLE_device details: {self._ble_device.details}") | ||
|
||
async def async_press(self) -> None: | ||
"""Restore last state when added.""" | ||
_LOGGER.debug("Connecting") | ||
try: | ||
client = await establish_connection( | ||
client_class=BleakClient, device=self._ble_device, name=self._ble_device.address | ||
) | ||
except asyncio.TimeoutError: | ||
_LOGGER.error("Connection Timeout error") | ||
except BleakError as err: | ||
_LOGGER.error("Connection: BleakError: %s", err) | ||
|
||
try: | ||
_LOGGER.debug("Sending command") | ||
await client.write_gatt_char( | ||
UUID_CONTROL_MODE, | ||
bytearray.fromhex(MODES["stop"]), | ||
response=False, | ||
) | ||
await asyncio.sleep(0.1) | ||
except asyncio.TimeoutError: | ||
_LOGGER.error("Connection Timeout error") | ||
except BleakError as err: | ||
_LOGGER.error("Connection: BleakError: %s", err) | ||
await client.disconnect() | ||
_LOGGER.debug("Disconnected") | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Return the device info.""" | ||
return DeviceInfo( | ||
identifiers={(DOMAIN, self._mac)}, | ||
name=f"Petoneer SmartDot ({self._mac})", | ||
manufacturer="Petoneer", | ||
model="SmartDot", | ||
) |
Oops, something went wrong.