Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
verdel committed Mar 12, 2024
0 parents commit d92955e
Show file tree
Hide file tree
Showing 17 changed files with 615 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @verdel
10 changes: 10 additions & 0 deletions .github/dependabot.yaml
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
33 changes: 33 additions & 0 deletions .github/workflows/hacs.yaml
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 added .gitignore
Empty file.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
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
100 changes: 100 additions & 0 deletions README.md
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).
Binary file added assets/petoneer-smartdot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added custom_components/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions custom_components/petoneer_smartdot/__init__.py
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
153 changes: 153 additions & 0 deletions custom_components/petoneer_smartdot/button.py
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",
)
Loading

0 comments on commit d92955e

Please sign in to comment.