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 Contrib support for Inverter Heat Pump such as Fairland IPHR55 #368

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
82 changes: 81 additions & 1 deletion testcontrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,86 @@
print(" * %s" % i)

print(" Test ThermostatDevice init(): ")
d = Contrib.ThermostatDevice( 'abcdefghijklmnop123456', '172.28.321.475', '1234567890123abc' )
d = Contrib.ThermostatDevice("abcdefghijklmnop123456", "172.28.321.475", "1234567890123abc")

import time
import os

# Load environment variables from .env file
try:
from dotenv import load_dotenv
load_dotenv()
except ModuleNotFoundError:
pass # dotenv not installed, ignore

IHP_DEVICEID = os.getenv("IHP_DEVICEID", None)
IHP_DEVICEIP = os.getenv("IHP_DEVICEIP", None)
IHP_DEVICEKEY = os.getenv("IHP_DEVICEKEY", None)
IHP_DEVICEVERS = os.getenv("IHP_DEVICEVERS", None)

if IHP_DEVICEID and IHP_DEVICEIP and IHP_DEVICEKEY and IHP_DEVICEVERS:
print(" Test InverterHeatPumpDevice: ")
print(" * Device ID: %s" % IHP_DEVICEID)
print(" * Device IP: %s" % IHP_DEVICEIP)
print(" * Device Key: %s" % IHP_DEVICEKEY)
print(" * Device Version: %s" % IHP_DEVICEVERS)
print()

device = Contrib.InverterHeatPumpDevice(
dev_id=IHP_DEVICEID, address=IHP_DEVICEIP, local_key=IHP_DEVICEKEY, version=IHP_DEVICEVERS
)

is_on = device.is_on()
unit = device.get_unit()
target_water_temp = device.get_target_water_temp()
lower_limit_target_water_temp = device.get_lower_limit_target_water_temp()
is_silence_mode = device.is_silence_mode()

print(" * is_on(): %r" % is_on)
print(" * get_unit(): %r" % unit)
print(" * get_inlet_water_temp(): %r" % device.get_inlet_water_temp())
print(" * get_target_water_temp(): %r" % target_water_temp)
print(" * get_lower_limit_target_water_temp(): %r" % lower_limit_target_water_temp)
print(" * get_upper_limit_target_water_temp(): %r" % device.get_upper_limit_target_water_temp())
print(" * get_heating_capacity_percent(): %r" % device.get_heating_capacity_percent())
print(" * get_mode(): %r" % device.get_mode())
print(" * get_mode(raw=True): %r" % device.get_mode(raw=True))
print(" * get_fault(): %r" % device.get_fault())
print(" * get_fault(raw=True): %r" % device.get_fault(raw=True))
print(" * is_silence_mode(): %r" % is_silence_mode)

time.sleep(10)

print(" Toggle ON/OFF")
for power_state in [not is_on, is_on]:
print(" * Turning %s" % ("ON" if power_state else "OFF"))
device.turn_on() if power_state else device.turn_off()
time.sleep(5)
print(" * is_on(): %r" % device.is_on())
time.sleep(10)

print(" Toggle unit")
for unit_value in [not unit.value, unit.value]:
print(" * Setting unit to %r" % Contrib.TemperatureUnit(unit_value))
device.set_unit(Contrib.TemperatureUnit(unit_value))
time.sleep(5)
print(" * get_unit(): %r" % device.get_unit())
time.sleep(5)

print(" Set target water temperature to lower limit and previous value")
for target_water_temp_value in [lower_limit_target_water_temp, target_water_temp]:
print(" * Setting target water temperature to %r" % target_water_temp_value)
device.set_target_water_temp(target_water_temp_value)
time.sleep(5)
print(" * get_target_water_temp(): %r" % device.get_target_water_temp())
time.sleep(5)

print(" Toggle silence mode")
for silence_mode in [not is_silence_mode, is_silence_mode]:
print(" * Setting silence mode to %r" % silence_mode)
device.set_silence_mode(silence_mode)
time.sleep(5)
print(" * is_silence_mode(): %r" % device.is_silence_mode())
time.sleep(5)

exit()
187 changes: 187 additions & 0 deletions tinytuya/Contrib/InverterHeatPumpDevice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""
Python module to interface with Tuya WiFi smart inverter heat pump

Author: Valentin Dusollier (https://github.com/valentindusollier)
Tested: Fairland Inverter+ 21kW (IPHR55)

Local Control Classes
InverterHeatPumpDevice(...)
See Device() for constructor arguments

Functions
InverterHeatPumpDevice:
is_on() # Returns True if the inverter is on
get_unit() # Returns the unit of the temperature
# (TemperatureUnit.CELSIUS or TemperatureUnit.FAHRENHEIT)
get_inlet_water_temp() # Returns the inlet water temperature
get_target_water_temp() # Returns the target water temperature
get_lower_limit_target_water_temp() # Returns the lower limit of the target water temperature
get_upper_limit_target_water_temp() # Returns the upper limit of the target water temperature
get_heating_capacity_percent() # Returns the heating capacity in percent
get_mode(raw=True/False) # Returns the current InverterHeatPumpMode(Enum) if raw=False
# (default value), otherwise returns the string mode
get_fault(raw=True/False) # Returns the current InverterHeatPumpFault(Enum) if raw=False
# (default value), otherwise returns the integer fault code
is_silence_mode() # Returns True if the silence mode is on

set_unit(TemperatureUnit) # Set the unit of the temperature
# (TemperatureUnit.CELSIUS or TemperatureUnit.FAHRENHEIT)
set_target_water_temp(integer) # Set the target water temperature. Must be between
# get_lower_limit_target_water_temp() and
# get_upper_limit_target_water_temp()
set_silence_mode(True/False) # Set the silence mode on (True) or off (False)

Inherited
json = status() # returns json payload
set_version(version) # 3.1 [default] or 3.3
set_socketPersistent(False/True) # False [default] or True
set_socketNODELAY(False/True) # False or True [default]
set_socketRetryLimit(integer) # retry count limit [default 5]
set_socketTimeout(timeout) # set connection timeout in seconds [default 5]
set_dpsUsed(dps_to_request) # add data points (DPS) to request
add_dps_to_request(index) # add data point (DPS) index set to None
set_retry(retry=True) # retry if response payload is truncated
set_status(on, switch=1, nowait) # Set status of switch to 'on' or 'off' (bool)
set_value(index, value, nowait) # Set int value of any index.
heartbeat(nowait) # Send heartbeat to device
updatedps(index=[1], nowait) # Send updatedps command to device
turn_on(switch=1, nowait) # Turn on device / switch #
turn_off(switch=1, nowait) # Turn off
set_timer(num_secs, nowait) # Set timer for num_secs
set_debug(toggle, color) # Activate verbose debugging output
set_sendWait(num_secs) # Time to wait after sending commands before pulling response
detect_available_dps() # Return list of DPS available from device
generate_payload(command, data) # Generate TuyaMessage payload for command with data
send(payload) # Send payload to device (do not wait for response)
receive()

Additional Classes
TemperatureUnit(Enum)
Enum to represent the unit of the temperature (C° or F°)

ExtendedEnum(Enum)
Internal use only.

InverterHeatPumpMode(ExtendedEnum)
Enum to represent the mode of the inverter. There is no documentation
about the modes, therefore only the known ones are listed. Feel free
to contribute if you know more about these modes.

InverterHeatPumpFault(ExtendedEnum)
Enum to represent the fault of the inverter. There is no documentation
about the fault codes, therefore only the known ones are listed. Feel
free to contribute if you know more about these codes.
"""

from enum import Enum
from ..core import Device


class InverterHeatPumpDevice(Device):

DPS = "dps"
ON_DP = "1"
INLET_WATER_TEMP_DP = "102"
UNIT_DP = "103"
HEATING_CAPACITY_PERCENT_DP = "104"
MODE_DP = "105"
TARGET_WATER_TEMP_DP = "106"
LOWER_LIMIT_TARGET_WATER_TEMP_DP = "107"
UPPER_LIMIT_TARGET_WATER_TEMP_DP = "108"
FAULT_DP = "115"
FAULT2_DP = "116"
SILENCE_MODE_DP = "117"

def is_on(self):
return self.status()[self.DPS][self.ON_DP]

def get_unit(self):
return TemperatureUnit(self.status()[self.DPS][self.UNIT_DP])

def get_inlet_water_temp(self):
return self.status()[self.DPS][self.INLET_WATER_TEMP_DP]

def get_target_water_temp(self):
return self.status()[self.DPS][self.TARGET_WATER_TEMP_DP]

def get_lower_limit_target_water_temp(self):
return self.status()[self.DPS][self.LOWER_LIMIT_TARGET_WATER_TEMP_DP]

def get_upper_limit_target_water_temp(self):
return self.status()[self.DPS][self.UPPER_LIMIT_TARGET_WATER_TEMP_DP]

def get_heating_capacity_percent(self):
return self.status()[self.DPS][self.HEATING_CAPACITY_PERCENT_DP]

def get_mode(self, raw=False):
"""There is no documentation about the modes. Therefore, your device
could push unkown modes and this method will return
InverterHeatPumpMode.UNKNOWN. You can use raw=True to get pushed value.
Feel free to contribute if you get unknown modes.
"""
string_mode = self.status()[self.DPS][self.MODE_DP]

if raw:
return string_mode

if InverterHeatPumpMode.is_known(string_mode):
return InverterHeatPumpMode(string_mode)

return InverterHeatPumpMode.UNKNOWN

def get_fault(self, raw=False):
"""There is no documentation about the fault codes. Therefore, your
device could push unkown fault codes and this method will return
InverterHeatPumpFault.UNKNOWN. You can use raw=True to get pushed value.
Feel free to contribute if you get unknown fault codes.
"""
fault = self.status()[self.DPS][self.FAULT_DP]

if raw:
return fault

if InverterHeatPumpFault.is_known(fault):
return InverterHeatPumpFault(fault)

return InverterHeatPumpFault.UNKNOWN

def is_silence_mode(self):
"""Paradoxically, the silence mode is on when SILENCE_MODE_DP is False"""
return not self.status()[self.DPS][self.SILENCE_MODE_DP]

def set_unit(self, unit):
self.set_value(self.UNIT_DP, unit.value)

def set_target_water_temp(self, target_water_temp):
sts = self.status()[self.DPS]
lower_limit, upper_limit = (
sts[self.LOWER_LIMIT_TARGET_WATER_TEMP_DP],
sts[self.UPPER_LIMIT_TARGET_WATER_TEMP_DP],
)
if lower_limit <= target_water_temp <= upper_limit:
self.set_value(self.TARGET_WATER_TEMP_DP, target_water_temp)
else:
raise ValueError("Target water temperature must be between {} and {}".format(lower_limit, upper_limit))

def set_silence_mode(self, silence_mode):
"""Paradoxically, the silence mode is on when SILENCE_MODE_DP is False"""
self.set_value(self.SILENCE_MODE_DP, not silence_mode)

class TemperatureUnit(Enum):
CELSIUS = True
FAHRENHEIT = False

class ExtendedEnum(Enum):
@classmethod
def is_known(self, value):
return value in self._value2member_map_

class InverterHeatPumpMode(ExtendedEnum):
UNKNOWN = "unknown"
HEATING = "warm"


class InverterHeatPumpFault(ExtendedEnum):
UNKNOWN = -1
NOMINAL = 0
NO_WATER_FLOW = 4
23 changes: 23 additions & 0 deletions tinytuya/Contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@ In addition to the built-in `OutletDevice`, `BulbDevice` and `CoverDevice` devic
d.set_timer(2)
```

### InverterHeatPumpDevice

* InverterHeatPumpDevice - A community-contributed Python module to add support for Tuya WiFi smart inverter heat pump
* Author: [Valentin Dusollier](https://github.com/valentindusollier)
* Tested: Fairland Inverter+ 21kW (IPHR55)

```python
from tinytuya import Contrib

device = Contrib.InverterHeatPumpDevice(dev_id="devid", address="ip", local_key="key", version="3.3")

device.set_unit(Contrib.TemperatureUnit.CELSIUS)

if device.get_fault() != Contrib.InverterHeatPumpFault.NOMINAL:
print("The inverter can't work normally. Turning off...")
device.turn_off()
exit()

if device.get_inlet_water_temp() < 26:
device.set_silence_mode(True)
device.set_target_water_temp(28)
```

## Submit Your Device

* We welcome new device modules!
Expand Down
3 changes: 2 additions & 1 deletion tinytuya/Contrib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
from .DoorbellDevice import DoorbellDevice
from .ClimateDevice import ClimateDevice
from .AtorchTemperatureControllerDevice import AtorchTemperatureControllerDevice
from .InverterHeatPumpDevice import InverterHeatPumpDevice, TemperatureUnit, InverterHeatPumpMode, InverterHeatPumpFault

DeviceTypes = ["ThermostatDevice", "IRRemoteControlDevice", "SocketDevice", "DoorbellDevice", "ClimateDevice", "AtorchTemperatureControllerDevice"]
DeviceTypes = ["ThermostatDevice", "IRRemoteControlDevice", "SocketDevice", "DoorbellDevice", "ClimateDevice", "AtorchTemperatureControllerDevice", "InverterHeatPumpDevice"]
Loading