Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bedjet as device instead of entity #8

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3c64f22
Update manifest.json
asheliahut Mar 28, 2023
ea5457c
update to allow m1-m3 but call m3 chill
asheliahut Mar 28, 2023
74ed17a
fix readme to new location
asheliahut Mar 28, 2023
25023da
fix chill and hvac mode
asheliahut Mar 28, 2023
16e8c61
add in device registration
asheliahut Mar 28, 2023
6229cc7
switch to new rewrite
asheliahut Mar 29, 2023
169fbee
rename bedjet.py to climate.py
asheliahut Mar 29, 2023
2acdd02
fix bugs with config
asheliahut Mar 29, 2023
9bd68e1
add in placeholder text for mac
asheliahut Mar 29, 2023
d9d10c5
cleanup and bugfixes
asheliahut Mar 29, 2023
96f4a70
bump version
asheliahut Mar 29, 2023
0ecbf5a
more cleanup
asheliahut Mar 29, 2023
585f8e5
version bump
asheliahut Mar 29, 2023
fc4ec5f
is it not async???
asheliahut Mar 29, 2023
87d146c
version bump
asheliahut Mar 29, 2023
e89a9b8
fix enumeration to include index
asheliahut Mar 29, 2023
8c68998
version bump
asheliahut Mar 29, 2023
3816b7c
missed enumeration
asheliahut Mar 29, 2023
2e5b649
testing changes
asheliahut Mar 29, 2023
da39ffb
more testing for data
asheliahut Mar 29, 2023
96874eb
add translations file
asheliahut Mar 29, 2023
c42f8df
reconnect to bedjets
asheliahut Mar 29, 2023
edddece
switch back to 0.2 and m3
asheliahut Mar 29, 2023
8a492e6
add back in more detail to the device_schema
asheliahut Mar 29, 2023
d7b765e
revert back schema change
asheliahut Mar 29, 2023
6dbce93
Move to new UUIDs
asheliahut Jun 7, 2023
153ab24
Testing UUIDS
asheliahut Jun 7, 2023
c2edb9c
Reverting UUID Changes
asheliahut Jun 7, 2023
81f68cd
Setup for unload/reload, increase reconnect timer, rename integration…
bphillips09 Nov 21, 2023
8fff8f0
Fix name in strings
bphillips09 Nov 21, 2023
14a078c
Fix for unloading
bphillips09 Nov 21, 2023
1569d3c
Unload forwarded entry
bphillips09 Nov 21, 2023
9050840
Fix dry state, rename fan modes for consistency
bphillips09 Nov 22, 2023
17d39a0
Apply suggestions from code review
asheliahut Dec 8, 2023
c453b1e
Merge pull request #2 from bphillips09/main
asheliahut Dec 8, 2023
0830c29
Fixes #3, Remove deprecations in bedjet
asheliahut Jan 7, 2024
a0f5d5b
add already supported functions to features list
asheliahut Feb 13, 2024
3c2b11c
update version
asheliahut Feb 13, 2024
3ac63f4
Improve Finding Bedjets
asheliahut May 25, 2024
19477f3
Update climate.py (#7)
asheliahut May 25, 2024
e7a8081
testing update for better bluetooth
asheliahut Aug 7, 2024
ac7e38a
update bedjet to use new version
asheliahut Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,20 @@ This project contains a climate entity that provides control of a [BedJet](https
## Installation (HACS) - Recommended

0. Have [HACS](https://custom-components.github.io/hacs/installation/manual/) installed, this will allow you to easily update
1. Add `https://github.com/robert-friedland/ha-bedjet` as a [custom repository](https://custom-components.github.io/hacs/usage/settings/#add-custom-repositories) as Type: Integration
1. Add `https://github.com/asheliahut/ha-bedjet` as a [custom repository](https://custom-components.github.io/hacs/usage/settings/#add-custom-repositories) as Type: Integration
2. Click install under "HA-BedJet", restart your instance.

## Installation (Manual)

1. Download this repository as a ZIP (green button, top right) and unzip the archive
2. Copy `/custom_components/ha_bedjet` to your `<config_dir>/custom_components/` directory
2. Copy `/custom_components/bedjet` to your `<config_dir>/custom_components/` directory
- You will need to create the `custom_components` folder if it does not exist
- On Hassio the final location will be `/config/custom_components/ha_bedjet`
- On Hassbian the final location will be `/home/homeassistant/.homeassistant/custom_components/ha_bedjet`
- On Hassio the final location will be `/config/custom_components/bedjet`
- On Hassbian the final location will be `/home/homeassistant/.homeassistant/custom_components/bedjet`

## Configuration

Add the following to your `configuration.yaml` file:

```yaml
# Example entry

climate:
- platform: ha_bedjet
```
Follow the configuration flow in devices & services

Configuration variables:

Expand Down
23 changes: 23 additions & 0 deletions custom_components/bedjet/.translations/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {
"step": {
"user": {
"title": "BedJet Configuration Update",
"description": "Enter the MAC address of your BedJet device (optional)."
},
"init": {
"title": "BedJet Configuration Initial",
"description": "Enter the MAC address of your BedJet device (optional)."
}
},
"error": {
"invalid_mac": "The MAC address provided is invalid. Please enter a valid MAC address.",
"cannot_connect": "Failed to connect to the BedJet device. Please check your connection and try again.",
"unknown": "An unknown error occurred. Please try again."
},
"abort": {
"already_configured": "This BedJet device is already configured. Please use the options menu to update the settings.",
"no_devices_found": "No BedJet devices were discovered. Please ensure your device is powered on and try again."
}
}
}
14 changes: 14 additions & 0 deletions custom_components/bedjet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
DOMAIN = "bedjet"

async def async_setup(hass, config):
return True

async def async_setup_entry(hass, entry):
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "climate")
)
return True

async def async_unload_entry(hass, entry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_forward_entry_unload(entry, "climate")
Original file line number Diff line number Diff line change
@@ -1,93 +1,94 @@
from homeassistant.const import (
TEMP_FAHRENHEIT,
ATTR_TEMPERATURE
)
from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_PRESET_MODE,
SUPPORT_FAN_MODE,
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_DRY
)
from enum import Enum
from datetime import datetime
import logging
from homeassistant.components import bluetooth
import asyncio
import voluptuous as vol
from bleak import BleakClient, BleakError
from bleak.backends.device import BLEDevice

from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
from homeassistant.const import (CONF_NAME, CONF_MAC)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import ClimateEntity
from homeassistant.const import CONF_MAC, UnitOfTemperature
from homeassistant.components import bluetooth
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity import DeviceInfo

from enum import Enum
from homeassistant.const import (
ATTR_TEMPERATURE
)
from homeassistant.components.climate.const import (
ClimateEntityFeature,
HVACMode as OriginalHVACMode
)

from . import DOMAIN
from .const import (BEDJET_COMMAND_UUID, BEDJET_COMMANDS,
BEDJET_SUBSCRIPTION_UUID)

from bleak import BleakClient, BleakError
from bleak.backends.device import BLEDevice

_LOGGER = logging.getLogger(__name__)

try:
from homeassistant.components.climate import (
ClimateEntity,
PLATFORM_SCHEMA,
)
except ImportError:
from homeassistant.components.climate import (
ClimateDevice as ClimateEntity,
PLATFORM_SCHEMA,
)

DISCOVERY_INTERVAL = 60 # seconds

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=''): cv.string,
vol.Optional(CONF_MAC, default=''): cv.string,
})
async def discover(hass):
while True:
service_infos = bluetooth.async_discovered_service_info(
hass, connectable=True)

bedjet_devices = [
service_info.device for service_info in service_infos if 'BEDJET' in service_info.name
]

async def async_setup_platform(hass, config, add_entities, discovery_info=None):
mac = config.get(CONF_MAC).upper()
if not bedjet_devices:
_LOGGER.warning("No BedJet devices were discovered.")
else:
_LOGGER.info(
f'Found {len(bedjet_devices)} BedJet{"" if len(bedjet_devices) == 1 else "s"}: {", ".join([d.address for d in bedjet_devices])}.'
)

if config.get(CONF_NAME) != '':
_LOGGER.warn(
f'{CONF_NAME} is a deprecated option and will be ignored.')
bedjets = [BedjetDeviceEntity(device) for idx, device in enumerate(bedjet_devices)]
if bedjets:
return bedjets

if not mac or mac == '':
_LOGGER.info('Setting up all discoverable BedJets.')
service_infos = bluetooth.async_discovered_service_info(
hass, connectable=True)
await asyncio.sleep(DISCOVERY_INTERVAL)

bedjet_devices = [
service_info.device for service_info in service_infos if service_info.name == 'BEDJET_V3']
async def async_setup_entry(hass, config_entry, async_add_entities):
mac = config_entry.data.get(CONF_MAC)
bedjets = await discover(hass)

_LOGGER.info(
f'Found {len(bedjet_devices)} BedJet{"" if len(bedjet_devices) == 1 else "s"}: {", ".join([d.address for d in bedjet_devices])}.')
# Check if the list of discovered devices is empty
if not bedjets:
_LOGGER.warning("No BedJet devices were discovered.")
return

bedjets = [BedJet(device) for device in bedjet_devices]
# Filter devices based on MAC address, if applicable
if mac is not None:
bedjets = [bj for bj in bedjets if bj.mac == mac]

else:
_LOGGER.info(f'Setting up BedJet with mac address {mac}.')
device = bluetooth.async_ble_device_from_address(
hass, mac, connectable=True)
bedjets = [BedJet(device)]
# Create BedjetDevice instance
bedjet_device = BedjetDevice(bedjets)

for bedjet in bedjets:
for bedjet in bedjet_device.entities:
asyncio.create_task(bedjet.connect_and_subscribe())

add_entities(bedjets)
# Add entities to Home Assistant
async_add_entities(bedjet_device.entities, True)

class BedjetDevice:
def __init__(self, bedjets):
self.entities = bedjets
self.number_of_entities = len(bedjets) if bedjets else 0

async def update(self):
for entity in self.entities:
await entity.update_data()

class FanMode(Enum):
FAN_MIN = 10
FAN_LOW = 25
FAN_MEDIUM = 50
FAN_HIGH = 75
FAN_MAX = 100
min = 10
low = 25
p45 = 45
medium = 50
p55 = 55
p60 = 60
high = 75
max = 100

@staticmethod
def get_fan_mode(fan_pct: int | None):
Expand All @@ -102,10 +103,10 @@ def get_fan_mode(fan_pct: int | None):


class HVACMode(Enum):
off = HVAC_MODE_OFF
cool = HVAC_MODE_COOL
heat = HVAC_MODE_HEAT
dry = HVAC_MODE_DRY
off = OriginalHVACMode.OFF
cool = OriginalHVACMode.COOL
heat = OriginalHVACMode.HEAT
dry = OriginalHVACMode.DRY

def command(self):
return BEDJET_COMMANDS.get(self.value)
Expand All @@ -118,6 +119,9 @@ class PresetMode(Enum):
dry = HVACMode.dry.value
turbo = 'turbo'
ext_ht = 'ext_ht'
m1 = 'm1'
m2 = 'm2'
m3 = 'm3'

def to_hvac(self) -> HVACMode:
map = {
Expand All @@ -134,12 +138,11 @@ def to_hvac(self) -> HVACMode:
def command(self):
return BEDJET_COMMANDS.get(self.value)


class BedJet(ClimateEntity):
class BedjetDeviceEntity(ClimateEntity):
def __init__(self, device: BLEDevice):
self._mac: str = device.address
self._client: BleakClient = BleakClient(
device, disconnected_callback=self.on_disconnect)
self._mac: str = device.address
self._current_temperature: int | None = None
self._target_temperature: int | None = None
self._hvac_mode: HVACMode | None = None
Expand Down Expand Up @@ -206,20 +209,20 @@ def name(self) -> str:
return f'bedjet_{format_mac(self.mac)}'

@property
def unique_id(self) -> str:
return f'climate_{self.name}'
def unique_id(self):
return f"{format_mac(self.mac)}"

@property
def temperature_unit(self) -> str:
return TEMP_FAHRENHEIT
return UnitOfTemperature.FAHRENHEIT

@property
def hvac_modes(self) -> list[str]:
return [mode.value for mode in HVACMode]

@property
def supported_features(self):
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE
return ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF

@property
def preset_modes(self) -> list[str]:
Expand All @@ -236,6 +239,20 @@ def min_temp(self) -> int:
@property
def max_temp(self) -> int:
return 109

@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
identifiers={
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, self.unique_id)
},
name=self.name,
manufacturer="BedJet",
model="BedJet",
hw_version="3.0"
)

@current_temperature.setter
def current_temperature(self, value: int):
Expand Down Expand Up @@ -274,7 +291,7 @@ def last_seen(self, value: datetime):
self._last_seen = value

async def connect(self, max_retries=10):
reconnect_interval = 3
reconnect_interval = 10
for i in range(0, max_retries):
try:
_LOGGER.info(f'Attempting to connect to {self.mac}.')
Expand Down Expand Up @@ -342,10 +359,16 @@ def get_preset_mode(value) -> PresetMode:
if value[14] == 0x50 and value[13] == 0x2d:
return PresetMode.heat
if value[14] == 0x3e:
return PresetMode.heat
return PresetMode.dry
if value[14] == 0x43:
return PresetMode.ext_ht

if value[14] == 0x20:
return PresetMode.m1
if value[14] == 0x21:
return PresetMode.m2
if value[14] == 0x22:
return PresetMode.m3

def get_hvac_mode(value) -> HVACMode:
return get_preset_mode(value).to_hvac()

Expand Down Expand Up @@ -423,3 +446,7 @@ async def async_set_hvac_mode(self, hvac_mode: str):

async def async_set_preset_mode(self, preset_mode: str):
await self.set_mode(PresetMode(preset_mode).command())

async def update_data(self):
if not self.is_connected:
await self.connect_and_subscribe()
Loading