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

feat: heat pump mode (dual with only one switch) #246

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"python.pythonPath": "/usr/bin/python3",
"python.formatting.provider": "black",
"editor.formatOnSave": true,
"python.analysis.diagnosticSeverityOverrides": {},
"python.analysis.indexing": true,
"python.analysis.autoImportCompletions": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
}
}
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The `dual_smart_thermostat` is an enhanced version of generic thermostat impleme
| **Fan With Cooler mode** | ![fan](/docs/images/fan-custom.png) ![cool](/docs/images/snowflake-custom.png) | [docs](#fan-with-cooler-mode) |
| **Cooler Only mode** | ![cool](/docs/images/snowflake-custom.png) | [docs](#cooler-only-mode) |
| **Dry mode** | ![humidity](docs/images/water-percent-custom.png) | [docs](#dry-mode) |
| **Heat Pump mode** | ![haet/cool](docs/images/sun-snowflake-custom.png) | [docs](#heat-pump-one-switch-heatcool-mode) |
| **Floor Temperature Control** | ![heating-coil](docs/images/heating-coil-custom.png) ![snowflake-thermometer](docs/images/snowflake-thermometer-custom.png) ![thermometer-alert](docs/images/thermometer-alert-custom.png) | [docs](#floor-heating-temperature-control) |
| **Window/Door sensor integration** | ![window-open](docs/images/window-open-custom.png) ![window-open](docs/images/door-open-custom.png) ![chevron-right](docs/images/chevron-right-custom.png) ![timer-cog](docs/images/timer-cog-outline-custom.png) ![chevron-right](docs/images/chevron-right-custom.png) ![hvac-off](docs/images/hvac-off-custom.png)| [docs](#openings) |
| **Presets** | | [docs](#presets) |
Expand Down Expand Up @@ -155,6 +156,57 @@ moist_tolerance: 5
dry_tolerance: 5
```

### Heat Pump (one switch heat/cool) mode

This setup allows you to use a single switch for both heating and cooling. To enable this mode you define only a single switch for the heater and set the set youer heat pump's current state (heating or cooling) as for the [`heat_pump_cooling`](#heat_pump_cooling) attribute. This must be an entity id of a sensor that has a state of `heating` or `cooling`.

The entity can be an input buulean for manual control or an entity that provided by the heat pump.

```yaml
heater: switch.study_heat_pump
target_sensor: sensor.study_temperature
heat_pump_cooling: sensor.study_heat_pump_state
```

#### Heat Pump Hvac Modes

##### Heat-Cool Mode

```yaml
heater: switch.study_heat_pump
target_sensor: sensor.study_temperature
heat_pump_cooling: sensor.study_heat_pump_state
heat_cool_mode: true
```

**heating** _(heat_pump_cooling: false)_:
- heat/cool
- heat
- off

**cooling** _(heat_pump_cooling: true)_:
- heat/cool
- cool
- off

##### Single mode

```yaml
heater: switch.study_heat_pump
target_sensor: sensor.study_temperature
heat_pump_cooling: sensor.study_heat_pump_state
heat_cool_mode: false # <-- or not set
```

**heating** _(heat_pump_cooling: false)_:
- heat
- off

**cooling** _(heat_pump_cooling: true)_:
- cool
- off


## Openings

The `dual_smart_thermostat` can turn off heating or cooling if a window or door is opened and turn heating or cooling back on when the door or window is closed to save energy.
Expand Down Expand Up @@ -439,6 +491,11 @@ The internal values can be set by the component only and the external values can
- `heat_cool`
- `fan_only`

### heat_pump_cooling

_(optional) (string)_ "`entity_id` for the heat pump cooling state sensor, heat_pump_cooling.state must be `heating` or `cooling`."
enables [heat pump mode](#heat-pump-one-switch-heatcool-mode)

### min_temp

_(optional) (float)_
Expand Down
29 changes: 29 additions & 0 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ input_boolean:
name: Fan toggle
dryer_on:
name: Fan toggle
heat_pump_cool:
name: Heat Pump Heat toggle
window_open:
name: Window
window_open2:
Expand Down Expand Up @@ -125,6 +127,17 @@ switch:
data:
entity_id: input_boolean.dryer_on

heat_pump_cool:
value_template: "{{ is_state('input_boolean.heat_pump_cool', 'on') }}"
turn_on:
service: input_boolean.turn_on
data:
entity_id: input_boolean.heat_pump_cool
turn_off:
service: input_boolean.turn_off
data:
entity_id: input_boolean.heat_pump_cool

window:
value_template: "{{ is_state('input_boolean.window_open', 'on') }}"
turn_on:
Expand Down Expand Up @@ -465,6 +478,22 @@ climate:
target_temp_low: 18
humidity: 60

- platform: dual_smart_thermostat
name: Dual Heat Pump
unique_id: dual_heat_pump
heater: switch.heater
target_sensor: sensor.room_temp
heat_pump_cooling: switch.heat_pump_cool
heat_cool_mode: true
target_temp_step: 0.1
precision: 0.1
min_temp: 9
max_temp: 32
target_temp: 20
cold_tolerance: 0.3
hot_tolerance: 0.3


# - platform: dual_smart_thermostat
# name: AUX Heat Room
# unique_id: aux_heat_room
Expand Down
47 changes: 47 additions & 0 deletions custom_components/dual_smart_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
CONF_FAN_ON_WITH_AC,
CONF_FLOOR_SENSOR,
CONF_HEAT_COOL_MODE,
CONF_HEAT_PUMP_COOLING,
CONF_HEATER,
CONF_HOT_TOLERANCE,
CONF_HUMIDITY_SENSOR,
Expand Down Expand Up @@ -187,6 +188,10 @@
vol.Optional(CONF_MOIST_TOLERANCE): vol.Coerce(float),
}

HEAT_PUMP_SCHEMA = {
vol.Optional(CONF_HEAT_PUMP_COOLING): cv.entity_id,
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HEATER): cv.entity_id,
Expand Down Expand Up @@ -238,6 +243,8 @@

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HEAT_PUMP_SCHEMA)

# Add the old presets schema to avoid breaking change
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(v): vol.Coerce(float) for (k, v) in CONF_PRESETS_OLD.items()}
Expand All @@ -260,6 +267,7 @@ async def async_setup_platform(
sensor_outside_entity_id = config.get(CONF_OUTSIDE_SENSOR)
sensor_humidity_entity_id = config.get(CONF_HUMIDITY_SENSOR)
sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION)
sensor_heat_pump_cooling_entity_id = config.get(CONF_HEAT_PUMP_COOLING)
keep_alive = config.get(CONF_KEEP_ALIVE)

precision = config.get(CONF_PRECISION)
Expand Down Expand Up @@ -290,6 +298,7 @@ async def async_setup_platform(
sensor_outside_entity_id,
sensor_humidity_entity_id,
sensor_stale_duration,
sensor_heat_pump_cooling_entity_id,
keep_alive,
precision,
unit,
Expand Down Expand Up @@ -343,6 +352,7 @@ def __init__(
sensor_outside_entity_id,
sensor_humidity_entity_id,
sensor_stale_duration,
sensor_heat_pump_cooling_entity_id,
keep_alive,
precision,
unit,
Expand Down Expand Up @@ -378,6 +388,7 @@ def __init__(
self.sensor_floor_entity_id = sensor_floor_entity_id
self.sensor_outside_entity_id = sensor_outside_entity_id
self.sensor_humidity_entity_id = sensor_humidity_entity_id
self.sensor_heat_pump_cooling_entity_id = sensor_heat_pump_cooling_entity_id

self._keep_alive = keep_alive

Expand Down Expand Up @@ -473,6 +484,19 @@ async def async_added_to_hass(self) -> None:
)
)

if self.sensor_heat_pump_cooling_entity_id is not None:
_LOGGER.debug(
"Adding heat pump cooling sensor listener: %s",
self.sensor_heat_pump_cooling_entity_id,
)
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self.sensor_heat_pump_cooling_entity_id],
self._async_entity_heat_pump_cooling_changed_event,
)
)

if self._keep_alive:
self.async_on_remove(
async_track_time_interval(
Expand Down Expand Up @@ -868,6 +892,8 @@ def _set_temperatures_dual_mode(self, temperatures: TargetTemperatures) -> None:
temp_low = temperatures.temp_low
temp_high = temperatures.temp_high

self.hvac_device.on_target_temperature_change(temperatures)

if self.features.is_target_mode:
if temperature is None:
return
Expand Down Expand Up @@ -1028,6 +1054,27 @@ async def _async_sensor_humidity_changed(
await self._async_control_climate()
self.async_write_ha_state()

async def _async_entity_heat_pump_cooling_changed_event(
self, event: Event[EventStateChangedData]
) -> None:
data = event.data

self.hvac_device.on_entity_state_changed(data["entity_id"], data["new_state"])

await self._asyn_entity_heat_pump_cooling_changed(data["new_state"])
self._attr_hvac_modes = self.hvac_device.hvac_modes
self.async_write_ha_state()

async def _asyn_entity_heat_pump_cooling_changed(
self, new_state: State | None, trigger_control=True
) -> None:
"""Handle heat pump cooling changes."""
_LOGGER.info("Entity heat pump cooling change: %s", new_state)

if trigger_control:
await self._async_control_climate()
self.async_write_ha_state()

async def _check_device_initial_state(self) -> None:
"""Prevent the device from keep running if HVACMode.OFF."""
_LOGGER.debug("Checking device initial state")
Expand Down
1 change: 1 addition & 0 deletions custom_components/dual_smart_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
CONF_OPENINGS = "openings"
CONF_OPENINGS_SCOPE = "openings_scope"
CONF_HEAT_COOL_MODE = "heat_cool_mode"
CONF_HEAT_PUMP_COOLING = "heat_pump_cooling"

ATTR_PREV_TARGET = "prev_target_temp"
ATTR_PREV_TARGET_LOW = "prev_target_temp_low"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Hvac Action Reason Module"""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""HVAC controller module for Dual Smart Thermostat."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from datetime import timedelta
import logging
from typing import Callable

from homeassistant.core import HomeAssistant

from custom_components.dual_smart_thermostat.hvac_controller.generic_controller import (
GenericHvacController,
)
from custom_components.dual_smart_thermostat.managers.environment_manager import (
EnvironmentManager,
)
from custom_components.dual_smart_thermostat.managers.opening_manager import (
OpeningManager,
)

_LOGGER = logging.getLogger(__name__)


class CoolerHvacController(GenericHvacController):

def __init__(
self,
hass: HomeAssistant,
entity_id,
min_cycle_duration: timedelta,
environment: EnvironmentManager,
openings: OpeningManager,
turn_on_callback: Callable,
turn_off_callback: Callable,
) -> None:
self._controller_type = self.__class__.__name__

super().__init__(
hass,
entity_id,
min_cycle_duration,
environment,
openings,
turn_on_callback,
turn_off_callback,
)
Loading