Skip to content

Commit

Permalink
Merge pull request #322 from PeteRager/2024.4.0
Browse files Browse the repository at this point in the history
2024.4.0
  • Loading branch information
PeteRager authored Jun 6, 2024
2 parents 900785e + 84664e1 commit 5726df2
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 98 deletions.
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"python.testing.pytestArgs": [
"tests","--asyncio-mode=auto"
],
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.experiments.enabled": false,
"cSpell.words": [
"ASHRAE",
"automations",
Expand All @@ -24,4 +24,8 @@
"sysuptime"
],
"cSpell.enabled": false,
"black-formatter.args": ["--line-length", "120"],
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
}
}
15 changes: 10 additions & 5 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,9 @@ Away Mode. The **Away** preset will put the S30 system into Manual Away Mode. Yo

Cancel Manual Away Mode or Smart Away Mode. The **cancel away mode** preset will cancel the active away mode (manual or smart) and return the S30 system to whatever state it was in prior to putting it into away mode. This works the same as if you pressed the cancel away icon on the S30 panel.

**Emergency Heat** - Lennox systems that have a heat pump and an auxiliary furnace, have an additional HVAC_MODE to run just the auxiliary furnace. In the S30 App this is shown as Emergency Heat. Home Assistant **does not** allow this mode directly - instead Home Assistant provides support for turning aux_heat on and off - independent of the HVAC_MODE. The integration has the following behavior:
**Emergency Heat** - Lennox systems that have a heat pump and an auxiliary furnace, have an additional HVAC_MODE to run just the auxiliary furnace. In the S30 App this is shown as Emergency Heat. Home Assistant **does not** allow this mode directly - instead a **select.system_zone_hvac_mode** entity provides support for setting the hvac mode to emergency heat. The integration has the following behavior:

- If Emergency Heat Mode is set in the S30, the HA climate will show Heat and the Aux Heat switch will be on.
- If the HA Aux Switch is turned on, the S30 Heat Mode will be set to **Emergency Heat**
- If the HA Aux Switch is turned off, the S30 Heat Mode will be set to **Heat**
- If Emergency Heat Mode is set in the S30, the HA climate will show Heat and the select entity will show **Emergency Heat**.
- If the Lennox Auxiliary Heat is running, the **aux** attribute in the HA Climate entity will be set to True and the HA Climate Entity will show **Heating**

**Humidification and Dehumidification**
Expand Down Expand Up @@ -474,11 +472,18 @@ Local Connections Only. Reports the WIFI signal strength, this sensor can be us

## Select Entities

### HVAC Mode

If your Lennox System has Emergency Heat a select entity will be created to allow you to set the hvac_mode.

select.<system_name><zone_name>\_hvac_mode


### Humidity Mode

If your Lennox System has Humidification or Dehumidification capability, one of these select entities is created per zone to allow you to set the active humidity mode. The available options - depending on your equipment - are **Dehumidify**, **Humidify** and **Off**

select.<system_name><zone_name>\_humidity_mode
select.<system_name>\_<zone_name>\_humidity_mode

### Dehumidification Modes

Expand Down
10 changes: 9 additions & 1 deletion custom_components/lennoxs30/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# pylint: disable=unused-argument
# pylint: disable=line-too-long
# pylint: disable=invalid-name
# pylint: disable=abstract-method

from __future__ import annotations

Expand Down Expand Up @@ -99,7 +100,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e

class S30Climate(S30BaseEntityMixin, ClimateEntity):
"""Class for Lennox S30 thermostat."""

def __init__(self, hass, manager: Manager, system: lennox_system, zone: lennox_zone):
"""Initialize the climate device."""
super().__init__(manager, system)
Expand Down Expand Up @@ -603,9 +603,16 @@ def is_aux_heat(self) -> bool | None:
res = self._zone.systemMode == LENNOX_HVAC_EMERGENCY_HEAT
return res

def _create_aux_heat_issue(self, service: str):
_LOGGER.warning(
"climate.%s is deprecated and will be removed in version 2024.10 learn more https://github.com/PeteRager/lennoxs30/blob/master/docs/aux_heat.md", service
)


async def async_turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
_LOGGER.info("climate:async_turn_aux_heat_on zone [%s]", self._myname)
self._create_aux_heat_issue("turn_aux_heat_on")
if self.is_zone_disabled:
raise HomeAssistantError(f"Unable to turn_aux_heat_on mode as zone [{self._myname}] is disabled")
try:
Expand All @@ -620,6 +627,7 @@ async def async_turn_aux_heat_on(self):

async def async_turn_aux_heat_off(self):
_LOGGER.info("climate:async_turn_aux_heat_off zone [%s]", self._myname)
self._create_aux_heat_issue("turn_aux_heat_off")
# When Aux is turned off, we will revert the zone to Heat Mode.
if self.is_zone_disabled:
raise HomeAssistantError(f"Unable to turn_aux_heat_on mode as zone [{self._myname}] is disabled")
Expand Down
2 changes: 2 additions & 0 deletions custom_components/lennoxs30/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
UNIQUE_ID_SUFFIX_BLE_COMMSTATUS: Final = "_BLE_COMMSTATUS"
UNIQUE_ID_SUFFIX_VENTILATION_SELECT: Final = "_VENT_SELECT"
UNIQUE_ID_SUFFIX_WIFI_RSSI: Final = "_WIFI_RSSI"
UNIQUE_ID_SUFFIX_EMERGENCY_HEAT: Final = "_EHEAT"
UNIQUE_ID_SUFFIX_ZONEMODE_SELECT: Final = "_ZONE_MODE_SELECT"

VENTILATION_EQUIPMENT_ID = -900

Expand Down
4 changes: 2 additions & 2 deletions custom_components/lennoxs30/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "local_push",
"issue_tracker" : "https://github.com/PeteRager/lennoxs30/issues",
"quality_scale": "platinum",
"requirements": ["lennoxs30api==0.2.14"],
"version": "2024.3.0"
"requirements": ["lennoxs30api==0.2.15"],
"version": "2024.6.0"
}
114 changes: 114 additions & 0 deletions custom_components/lennoxs30/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
# pylint: disable=unused-argument
# pylint: disable=line-too-long
# pylint: disable=invalid-name
# pylint: disable=abstract-method
from typing import Any
import logging


from homeassistant.components.climate.const import HVACMode
from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
Expand All @@ -20,6 +22,8 @@
LENNOX_HUMIDITY_MODE_OFF,
LENNOX_HUMIDITY_MODE_HUMIDIFY,
LENNOX_HUMIDITY_MODE_DEHUMIDIFY,
LENNOX_HVAC_EMERGENCY_HEAT,
LENNOX_HVAC_HEAT_COOL,
LENNOX_DEHUMIDIFICATION_MODE_HIGH,
LENNOX_DEHUMIDIFICATION_MODE_MEDIUM,
LENNOX_DEHUMIDIFICATION_MODE_AUTO,
Expand Down Expand Up @@ -48,6 +52,7 @@
MANAGER,
UNIQUE_ID_SUFFIX_EQ_PARAM_SELECT,
UNIQUE_ID_SUFFIX_VENTILATION_SELECT,
UNIQUE_ID_SUFFIX_ZONEMODE_SELECT,
)
from . import DOMAIN, Manager

Expand Down Expand Up @@ -89,6 +94,13 @@ async def async_setup_entry(
select = EquipmentParameterSelect(hass, manager, system, equipment, parameter)
select_list.append(select)

for zone in system.zone_list:
if zone.is_zone_active():
if zone.emergencyHeatingOption or system.has_emergency_heat():
zone_emergency_heat = ZoneModeSelect(hass, manager, system, zone)
select_list.append(zone_emergency_heat)


if len(select_list) != 0:
async_add_entities(select_list, True)

Expand Down Expand Up @@ -459,3 +471,105 @@ def extra_state_attributes(self):
attrs: dict[str, Any] = {}
attrs["installer_settings"] = self._system.ventilationControlMode
return attrs

class ZoneModeSelect(S30BaseEntityMixin, SelectEntity):
"""Set the zone hvac mode"""

def __init__(
self,
hass: HomeAssistant,
manager: Manager,
system: lennox_system,
zone: lennox_zone,
):
super().__init__(manager, system)
self.hass: HomeAssistant = hass
self._zone = zone
self._myname = f"{self._system.name}_{self._zone.name}_hvac_mode"
_LOGGER.debug("Create ZoneModeSelect myname [%s]", self._myname)

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
_LOGGER.debug("async_added_to_hass VentilationModeSelect myname [%s]", self._myname)
self._zone.registerOnUpdateCallback(self.zone_update_callback, ["systemMode"])
self._system.registerOnUpdateCallback(self.system_update_callback, ["zoningMode"])
await super().async_added_to_hass()

def zone_update_callback(self):
"""Callback for system updates"""
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"zone_update_callback ZoneModeSelect myname [%s]",
self._myname,
)
self.schedule_update_ha_state()

def system_update_callback(self):
"""Callback for system updates"""
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"system_update_callback ZoneModeSelect myname [%s]",
self._myname,
)
self.schedule_update_ha_state()


@property
def unique_id(self) -> str:
# HA fails with dashes in IDs
return self._zone.unique_id + UNIQUE_ID_SUFFIX_ZONEMODE_SELECT

@property
def name(self):
return self._myname

@property
def current_option(self) -> str:
r = self._zone.getSystemMode()
if r == LENNOX_HVAC_HEAT_COOL:
r = HVACMode.HEAT_COOL
return r

@property
def options(self) -> list:
modes = []
if self._zone.is_zone_disabled:
return modes
modes.append(HVACMode.OFF)
if self._zone.coolingOption:
modes.append(HVACMode.COOL)
if self._zone.heatingOption:
modes.append(HVACMode.HEAT)
if self._zone.coolingOption and self._zone.heatingOption:
modes.append(HVACMode.HEAT_COOL)
if self._zone.emergencyHeatingOption or self._system.has_emergency_heat():
modes.append(LENNOX_HVAC_EMERGENCY_HEAT)
return modes

async def async_select_option(self, option: str) -> None:
_LOGGER.info(LOG_INFO_SELECT_ASYNC_SELECT_OPTION, self.__class__.__name__, self._myname, option)
if self._zone.is_zone_disabled:
raise HomeAssistantError(f"Unable to set hvac_mode as zone [{self._myname}] is disabled")
try:
hvac_mode = option if option != HVACMode.HEAT_COOL else LENNOX_HVAC_HEAT_COOL
_LOGGER.info(
"select:async_set_option [%s] ha_mode [%s] lennox_mode [%s]",
self._myname,
option,
hvac_mode,
)
await self._zone.setHVACMode(hvac_mode)
except S30Exception as ex:
raise HomeAssistantError(f"async_select_option [{self._myname}] [{ex.as_string()}]") from ex
except Exception as ex:
raise HomeAssistantError(
f"async_select_option unexpected exception, please log issue, [{self._myname}] exception [{ex}]"
) from ex

@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
result = {
"identifiers": {(DOMAIN, self._zone.unique_id)},
}
return result
2 changes: 1 addition & 1 deletion custom_components/lennoxs30/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,5 @@
"title": "Options"
}
}
}
}
}
25 changes: 25 additions & 0 deletions docs/aux_heat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Aux Heat

## Overview

Home Assistant has deprecated aux_heat and will remove it from the product in 2024.10.0

This integration will continue to support aux heat until 2024.10.0

If you are using aux heat you will need to migrate to use the new functionality before that date.

## Changes

A new select entity is created for each zone for systems that have aux heat. The name of the entity is **select.[system_name]_[zone_name]_hvac_mode()**

This select entity will have all the valid hvac_modes for your system including **emergency heat**. You can select emergency heat directly from the drop down or use the service call:

```yaml
service: select.select_option
target:
entity_id: select.ragehouse_zone_1_hvac_mode
data:
option: "emergency heat"
```
The climate entity hvac_mode will be heat when the lennox hvac_mode is heat or emergency heat. Changing the hvac_mode in the climate entity will cause the select to update.
22 changes: 5 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@

pytest_plugins = "pytest_homeassistant_custom_component"


@pytest.fixture(autouse=True)
def socket_enabled():
pass

@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
yield
Expand Down Expand Up @@ -131,8 +126,7 @@ def loadfile(name: str, sysId: str = None) -> json:

@pytest.fixture
def config_entry_local() -> config_entries.ConfigEntry:
config = config_entries.ConfigEntry(version=1, domain=DOMAIN, title="10.0.0.1", data={}, source="User")
config.unique_id = "12345"
config = config_entries.ConfigEntry(version=1, minor_version=0, domain=DOMAIN, title="10.0.0.1", data={}, source="User", unique_id="12345")
config.data = {}
config.data[CONF_CLOUD_CONNECTION] = False
config.data[CONF_HOST] = "10.0.0.1"
Expand All @@ -158,8 +152,7 @@ def config_entry_local() -> config_entries.ConfigEntry:

@pytest.fixture
def config_entry_cloud() -> config_entries.ConfigEntry:
config = config_entries.ConfigEntry(version=1, domain=DOMAIN, title="10.0.0.1", data={}, source="User")
config.unique_id = "12345"
config = config_entries.ConfigEntry(version=1, minor_version = 0, domain=DOMAIN, title="10.0.0.1", data={}, source="User", unique_id="12345")
config.data = {}
config.data[CONF_CLOUD_CONNECTION] = True
config.data[CONF_EMAIL] = "[email protected]"
Expand Down Expand Up @@ -270,8 +263,7 @@ def manager_us_customary_units(hass: HomeAssistant, config_entry_local) -> Manag

@pytest.fixture
def manager_2_systems(hass) -> Manager:
config = config_entries.ConfigEntry(version=1, domain=DOMAIN, title="10.0.0.1", data={}, source="User")
config.unique_id = "12345"
config = config_entries.ConfigEntry(version=1, minor_version = 0, domain=DOMAIN, title="10.0.0.1", data={}, source="User", unique_id="12345")

manager_to_return = Manager(
hass=hass,
Expand Down Expand Up @@ -326,9 +318,7 @@ def manager_2_systems(hass) -> Manager:

@pytest.fixture
def manager_mz(hass) -> Manager:
config = config_entries.ConfigEntry(version=1, domain=DOMAIN, title="10.0.0.1", data={}, source="User")
config.unique_id = "12345"

config = config_entries.ConfigEntry(version=1, minor_version = 0, domain=DOMAIN, title="10.0.0.1", data={}, source="User", unique_id="12345")
manager_to_return = Manager(
hass=hass,
config=config,
Expand Down Expand Up @@ -371,9 +361,7 @@ def manager_mz(hass) -> Manager:

@pytest.fixture
def manager_system_04_furn_ac_zoning(hass) -> Manager:
config = config_entries.ConfigEntry(version=1, domain=DOMAIN, title="10.0.0.1", data={}, source="User")
config.unique_id = "12345"

config = config_entries.ConfigEntry(version=1, minor_version = 0, domain=DOMAIN, title="10.0.0.1", data={}, source="User", unique_id="12345")
manager_to_return = Manager(
hass=hass,
config=config,
Expand Down
Loading

0 comments on commit 5726df2

Please sign in to comment.