Skip to content

Commit

Permalink
Merge pull request #9 from pink88/dev
Browse files Browse the repository at this point in the history
Battery Status, Blind postition, refactors and bug fixes
  • Loading branch information
pink88 authored Feb 4, 2024
2 parents 2e78f43 + 79c32aa commit 6569901
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 254 deletions.
60 changes: 49 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,74 @@
# Tuiss2HA

This adds support for Blinds2go Electronic blinds, powered by the Tuiss Smartview platform (if other brands exist they should work, but are untested). These blinds use bluetoth low energy and are controlled through a simple cover interface.
This adds support for Blinds2go Electronic blinds, powered by the Tuiss Smartview platform (if other brands exist they should work, but are untested). These blinds use bluetooth low energy and are controlled through a simple cover interface. As well as control of the blinds position through home assistant, this also includes 2 services. 1 service can be used to get the battery status (normal or low). The other can be used to periodically poll your blinds to get their position, as using the Tuiss app or bluetooth remotes will not automatically update the position of the blind in home assistant due to limitations in the tuiss platform itself.


## Before Integration to Home Assistant ##
To get started you need to download and use the Tuiss Smartview app to set the upper and bottom limits of the blinds.

## Installation ##
## Installation and adding your first device ##
1. Add the integration from the HACs marketplace ([instructions here](https://hacs.xyz/docs/configuration/basic))
2. Restart Home Assistant if required
3. Click Settings
4. Click Integration
5. Click the + icon
4. Click Devices & Services
5. Click the "+ add integration" button
6. Select Tuiss SmartView from the dropdown
7. Enter the MAC address from the tag including in your blind, or written within the top bracket of the blind, close to where the battery is plugged in
8. Give the blind a name
9. Click Submit

## Features ##

### Currently Supported ###
- Set position
- Open
- Close
- Battery State (through service)
- Blind position (through service)

### Battery State ###
An accurate battery percentage is not provided by the blind, but it is possible to return two states:
1. "Battery is good"
2. "Battery needs charging"

To accomplish this a service has been provided: "Tuiss2ha.Get Battery Status". This can be called manually from Developers -> Services or included as a part of an automation which I'd recommend runs on a weekly schedule. The resulting battery state of "Normal" or "Low" is then recorded against the Battery entity. The automation can then send a notification if the battery state is returned as low from the service. For example:

alias: Blinds_Battery_Notify
description: ""
trigger:
- platform: time
at: "02:00:00"
condition: []
action:
- service: tuiss2ha.get_battery_status
target:
entity_id:
- binary_sensor.hallway_blind_battery
- binary_sensor.study_blind_battery
data: {}
- if:
- condition: state
entity_id: binary_sensor.hallway_blind_battery
state: "on"
then:
- service: notify.iPhone
data:
message: Hallway Blind battery is low
- if:
- condition: state
entity_id: binary_sensor.study_blind_battery
state: "on"
then:
- service: notify.iPhone
data:
message: Study battery is low`


### Planned Featuresure ###
- *Battery status* - this is not yet included, but being looked into
### Accurate Blind Position ###
The blind will not update its position within Home Assistant if controlled using the Tuiss app or bluetooth remotes. To compensate, a service "Tuiss2ha.Get Blind Position" has been provided. This can be called manually from Developers -> Services or included as a part of an automation. The automation can be set to run periodically throughout the day to update the position. I would not recommend running this more than hourly as it will likely drain your blinds batteries (though I have not tested this).


## Limitations ##
- *Open and Close status* - currently the opening and closing status of the blind is not included, it will only report on if the blind is Open and Closed, not while it is moving
- Currently the opening and closing status of the blind are not included, it will only report on if the blind is Open and Closed, not while it is moving


## Troubleshooting ##
- I've only testing with HAOS installed on a raspberry pi4b and the Bluetooth module built in did not work, so I had to use a couple ESP32 devices with Bluetooth proxy software installed (See [here](https://esphome.io/components/bluetooth_proxy.html))
- I've only tested with HAOS installed on a Raspberry Pi4b and the built in Bluetooth module did not work, so I had to use a couple ESP32 devices with Bluetooth proxy software installed (See [here](https://esphome.io/components/bluetooth_proxy.html))
- Sometimes the devices take a few attempts to connect, so expect some delay to commmands (though much improved from HA 2023-07 onwards if using ESPhome)
11 changes: 5 additions & 6 deletions custom_components/tuiss2ha/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
"""The Detailed Hello World Push integration."""
"""Tuiss2HA integration."""
from __future__ import annotations


from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from . import hub
from .const import DOMAIN



PLATFORMS: list[str] = ["cover"]
PLATFORMS: list[str] = ["cover","binary_sensor"]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hello World from a config entry."""
"""Set up Tuiss2HA from a config entry."""
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub.Hub(hass, entry.data["host"], entry.data["name"])

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


Expand All @@ -29,3 +27,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok

85 changes: 85 additions & 0 deletions custom_components/tuiss2ha/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Support for Battery sensors."""
from __future__ import annotations

import logging

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.restore_state import RestoreEntity

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up Tuiss2ha Battery sensor."""
hub = hass.data[DOMAIN][entry.entry_id]
sensors = []
for blind in hub.blinds:
sensors.append(BatterySensor(blind))
async_add_entities(sensors, True)

platform = entity_platform.async_get_current_platform()

platform.async_register_entity_service(
"get_battery_status", {}, async_get_battery_status
)


async def async_get_battery_status(entity, service_call):
"""Get the battery status when called by service."""
await entity._blind.get_battery_status()
entity._attr_is_on = entity._blind._battery_status
entity.schedule_update_ha_state()


class BatterySensor(BinarySensorEntity, RestoreEntity):
"""Battery sensor for Tuiss2HA Cover."""

should_poll = False

def __init__(self, blind) -> None:
"""Initialize the sensor."""
self._blind = blind
self._attr_unique_id = f"{self._blind.blind_id}_battery"
self._attr_name = f"{self._blind.name} Battery"
self._attr_device_class = BinarySensorDeviceClass.BATTERY

# To link this entity to the cover device, this property must return an
# identifiers value matching that used in the cover, but no other information such
# as name. If name is returned, this entity will then also become a device in the
# HA UI.
@property
def device_info(self):
"""Return information to link this entity with the correct device."""
return {"identifiers": {(DOMAIN, self._blind.blind_id)}}

@property
def device_class(self):
"""Return device class."""
return self._attr_device_class

async def async_added_to_hass(self):
"""Run when this Entity has been added to HA."""
last_state = await self.async_get_last_state()
_LOGGER.debug(last_state)
if last_state.state == "on":
self._attr_is_on = True
else:
self._attr_is_on = False

# Sensors should also register callbacks to HA when their state changes
self._blind.register_callback(self.async_write_ha_state)

async def async_will_remove_from_hass(self):
"""Entity being removed from hass."""
# The opposite of async_added_to_hass. Remove any registered call backs here.
self._blind.remove_callback(self.async_write_ha_state)
30 changes: 19 additions & 11 deletions custom_components/tuiss2ha/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@
from homeassistant import config_entries, exceptions
from homeassistant.core import HomeAssistant

from .const import DOMAIN # pylint:disable=unused-import
from .hub import Hub
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
{
vol.Required("host", default="XX:XX:XX:XX:XX:XX"): str,
vol.Required("name", default="Name for device"): str
vol.Required("name", default="Name for device"): str,
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tuiss2ha."""
"""Handle config flow for Tuiss2HA."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
Expand All @@ -36,11 +35,15 @@ async def async_step_user(self, user_input=None):
_title = await validate_input(self.hass, user_input)

return self.async_create_entry(title=_title, data=user_input)
except CannotConnect:
errors["name"] = "Cannot connect"
except InvalidHost:
errors[
"host"
] = "Your host should be a valid MAC address in the format XX:XX:XX:XX:XX:XX"
except InvalidName:
errors["name"] = "Your name must be longer than 0 characters"
except InvalidHost:
errors["host"] = "Your host should be a valid MAC address in the format XX:XX:XX:XX:XX:XX"
except Exception:
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

Expand All @@ -51,18 +54,23 @@ async def async_step_user(self, user_input=None):

async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect."""

if len(data["host"]) < 17:
raise InvalidHost

if len(data["name"]) == 0 :
if len(data["name"]) == 0:
raise InvalidName

return data["name"]


class InvalidName(exceptions.HomeAssistantError):
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidHost(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid hostname."""
"""Error to indicate there is an invalid hostname."""


class InvalidName(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid device name."""
5 changes: 3 additions & 2 deletions custom_components/tuiss2ha/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for the ha2tuiss integration."""

# This is the internal name of the integration, it should also match the directory
# name for the integration.
DOMAIN = "tuiss2ha"
DOMAIN = "tuiss2ha"
BATTERY_NOTIFY_CHARACTERISTIC = "00010304-0405-0607-0809-0a0b0c0d1910"
UUID = "00010405-0405-0607-0809-0a0b0c0d1910"
Loading

0 comments on commit 6569901

Please sign in to comment.