From 8599c3c14b290addb72424e235df9b9e811aa593 Mon Sep 17 00:00:00 2001 From: ggardet Date: Thu, 12 Dec 2024 08:59:51 +0100 Subject: [PATCH 1/2] Add DP table for WIFI Dual Meter --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 246728c..47e64f2 100644 --- a/README.md +++ b/README.md @@ -872,6 +872,51 @@ thermo = ThermostatDevice( 'abcdefghijklmnop123456', '172.28.321.475', '12345678 For info on the Sensor Data lists, see https://github.com/jasonacox/tinytuya/discussions/139 +#### Version 3.4 - WIFI Dual Meter (i.e. PJ1103A) +| DP ID | Function Point | Type | Range | Units | +| ------------- | ------------- | ------------- | ------------- |------------- | +|1|Forward energy total|integer||kWh x 100| +|2|Reverse energy total|integer||kWh x 100| +|101|Power (A)|integer||W x 10| +|102|Direction of current flow (A)| enum | || +|104|Direction of current flow (B)| enum | || +|105|Power (B)|integer||W x 10| +|106|Energy forward (A)|integer||kWh x 100| +|107|Energy reverse (A)|integer||kWh x 100| +|108|Energy forward (B)|integer||kWh x 100| +|109|Energy reverse (B)|integer||kWh x 100| +|110|Power Factor (A)|integer||value x 100| +|111|AC frequency|integer||Hz x 100| +|112|AC voltage|integer||V x 10| +|113|Current (A)|integer||mA| +|114|Current (B)|integer||mA| +|115|Total power|integer||W x 10| +|116|Voltage Calibration|integer|800-1200|value * 1000| +|117|Current Calibration (A)|integer|800-1200|value * 1000| +|118|Power Calibration (A)|integer|800-1200|value * 1000| +|119|Energy Calibration forward (A)|integer|800-1200|value * 1000| +|121|Power Factor (B)|integer||value x 100| +|122|Frequency Calibration|integer|800-1200|value * 1000| +|123|Current Calibration (B)|integer|800-1200|value * 1000| +|124|Power Calibration (B)|integer|800-1200|value * 1000| +|125|Energy Calibration forward (B)|integer|800-1200|value * 1000| +|127|Energy Calibration reverse (A)|integer|800-1200|value * 1000| +|128|Energy Calibration reverse (B)|integer|800-1200|value * 1000| +|129|Report Rate Control|integer|3-60|s| + +Note: (A) or (B) means channel A or channel B + +A user contributed module is available for this device in the [Contrib library](https://github.com/jasonacox/tinytuya/tree/master/tinytuya/Contrib): + +```python from tinytuya.Contrib import WiFiDualMeterDevice + +wdm = WiFiDualMeterDevice.WiFiDualMeterDevice( + dev_id='abcdefghijklmnop123456', + address='192.168.0.29', # Or set to 'Auto' to auto-discover IP address + local_key='1234567890123abc', + version=3.4) +``` + ### Tuya References * Tuya Hardware Development - Protocol: https://developer.tuya.com/en/docs/iot/device-development/embedded-software-development/mcu-development-access/wifi-mcu-sdk-solution/tuya-cloud-universal-serial-port-access-protocol?id=K9hhi0xxtn9cb From 7ea70cb9a73525ca9c268f2e308fab8e315dca0e Mon Sep 17 00:00:00 2001 From: Guillaume Gardet Date: Thu, 12 Dec 2024 12:26:08 +0100 Subject: [PATCH 2/2] contrib: add WiFiDualMeterDevice.py --- tinytuya/Contrib/README.md | 22 +++ tinytuya/Contrib/WiFiDualMeterDevice.py | 243 ++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 tinytuya/Contrib/WiFiDualMeterDevice.py diff --git a/tinytuya/Contrib/README.md b/tinytuya/Contrib/README.md index f847ba2..7232a1e 100644 --- a/tinytuya/Contrib/README.md +++ b/tinytuya/Contrib/README.md @@ -223,6 +223,28 @@ In addition to the built-in `OutletDevice`, `BulbDevice` and `CoverDevice` devic ``` +### WiFiDualMeterDevice + +* WiFiDualMeterDevice - A community-contributed Python module to add support for Tuya WiFi Dual Meter device +* Author: [Guillaume Gardet](https://github.com/ggardet) + +``` +from tinytuya.Contrib import WiFiDualMeterDevice + +wdm = WiFiDualMeterDevice.WiFiDualMeterDevice( + dev_id='YOUR_DEV_ID', + address='192.168.XX.YY', # Or set to 'Auto' to auto-discover IP address + local_key='LOCAL_KEY', + version=3.4) + +# Print all known values +wdm.print_all() + +# Only print Voltage and frequency +print(wdm.get_freq()) +print(wdm.get_voltage()) + + ## Submit Your Device * We welcome new device modules! diff --git a/tinytuya/Contrib/WiFiDualMeterDevice.py b/tinytuya/Contrib/WiFiDualMeterDevice.py new file mode 100644 index 0000000..c22f9a2 --- /dev/null +++ b/tinytuya/Contrib/WiFiDualMeterDevice.py @@ -0,0 +1,243 @@ +# TinyTuya WiFi Dual Meter Device +# -*- coding: utf-8 -*- +""" + Python module to interface with Tuya WiFi Dual Meter Devices + + Author: Guillaume Gardet + + Local Control Classes + WiFiDualMeterDevice(...) + See OutletDevice() for constructor arguments + + Functions + WiFiDualMeterDevice: + get_current_b() + get_total_power() + get_voltage_calibration() + get_current_calibration_a() + get_power_calibration_a() + get_energy_calibration_a() + get_power_factor_b() + get_current_calibration_b() + get_power_calibration_b() + get_energy_calibration_b() + get_energy_reverse_calibration_a() + get_energy_reverse_calibration_b() + get_report_rate() + 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() +""" + +from ..core import Device + +class WiFiDualMeterDevice(Device): + + DPS_FORWARD_ENERGY_TOTAL = '1' + DPS_REVERSE_ENERGY_TOTAL = '2' + DPS_POWER_A = '101' + DPS_DIR_CUR_A = '102' + DPS_DIR_CUR_B = '104' + DPS_POWER_B = '105' + DPS_ENERGY_FORWARD_A = '106' + DPS_ENERGY_REVERSE_A = '107' + DPS_ENERGY_FORWARD_B = '108' + DPS_ENERGY_REVERSE_B = '109' + DPS_POWER_FACTOR_A = '110' + DPS_FREQ = '111' + DPS_VOLTAGE = '112' + DPS_CURRENT_A = '113' + DPS_CURRENT_B = '114' + DPS_TOTAL_POWER = '115' + DPS_VOLTAGE_CALIBRATION = '116' + DPS_CURRENT_CALIBRATION_A = '117' + DPS_POWER_CALIBRATION_A = '118' + DPS_ENERGY_CALIBRATION_A = '119' + DPS_POWER_FACTOR_B = '121' + DPS_FREQUENCY_CALIBRATION = '122' + DPS_CURRENT_CALIBRATION_B = '123' + DPS_POWER_CALIBRATION_B = '124' + DPS_ENERGY_CALIBRATION_B = '125' + DPS_ENERGY_CALIBRATION_REVERSE_A = '127' + DPS_ENERGY_CALIBRATION_REVERSE_B = '128' + DPS_REPORT_RATE = '129' + + dps_data = { + DPS_FORWARD_ENERGY_TOTAL: { 'name': 'forward_energy_total', 'unit': 'kWh', 'scale': 100 }, + DPS_REVERSE_ENERGY_TOTAL: { 'name': 'reverse_energy_total', 'unit': 'kWh', 'scale': 100 }, + DPS_POWER_A: { 'name': 'power_a', 'unit': 'W', 'scale': 10 }, + DPS_DIR_CUR_A: { 'name': 'dir_curent_a', 'enum': ['FORWARD', 'REVERSE'] }, + DPS_DIR_CUR_B: { 'name': 'dir_current_b', 'enum': ['FORWARD', 'REVERSE'] }, + DPS_POWER_B: { 'name': 'power_b', 'unit': 'W', 'scale': 10 }, + DPS_ENERGY_FORWARD_A: { 'name': 'forward_energy_a', 'unit': 'kWh', 'scale': 100 }, + DPS_ENERGY_REVERSE_A: { 'name': 'reverse_energy_a', 'unit': 'kWh', 'scale': 100 }, + DPS_ENERGY_FORWARD_B: { 'name': 'forward_energy_b', 'unit': 'kWh', 'scale': 100 }, + DPS_ENERGY_REVERSE_B: { 'name': 'reverse_energy_b', 'unit': 'kWh', 'scale': 100 }, + DPS_POWER_FACTOR_A: { 'name': 'power_factor_a', 'scale': 100 }, + DPS_FREQ: { 'name': 'ac_frequency', 'unit': 'Hz', 'scale': 100 }, + DPS_VOLTAGE: { 'name': 'ac_voltage', 'unit': 'V', 'scale': 10 }, + DPS_CURRENT_A: { 'name': 'current_a', 'unit': 'mA'}, + DPS_CURRENT_B: { 'name': 'current_b', 'unit': 'mA'}, + DPS_TOTAL_POWER: { 'name': 'total_power', 'unit': 'W', 'scale': 10 }, + DPS_VOLTAGE_CALIBRATION : { 'name': 'voltage_calibration', 'scale': 1000 }, + DPS_CURRENT_CALIBRATION_A: { 'name': 'current_calibration_a', 'scale': 1000 }, + DPS_POWER_CALIBRATION_A: { 'name': 'power_calibration_a', 'scale': 1000 }, + DPS_ENERGY_CALIBRATION_A: { 'name': 'energy_calibration_a', 'scale': 1000 }, + DPS_POWER_FACTOR_B: { 'name': 'power_factor_b', 'scale': 100 }, + DPS_CURRENT_CALIBRATION_B: { 'name': 'current_calibration_b', 'scale': 1000 }, + DPS_POWER_CALIBRATION_B: { 'name': 'power_calibration_b', 'scale': 1000 }, + DPS_ENERGY_CALIBRATION_B: { 'name': 'energy_calibration_b', 'scale': 1000 }, + DPS_ENERGY_CALIBRATION_REVERSE_A: { 'name': 'energy_calibration_reverse_a', 'scale': 1000 }, + DPS_ENERGY_CALIBRATION_REVERSE_B: { 'name': 'energy_calibration_reverse_b', 'scale': 1000 }, + DPS_REPORT_RATE: { 'name': 'report_rate', 'unit': 's' }, + } + + def get_value(self, dps_code, status_data=None): + if status_data is None: + status_data = self.status() + name = self.dps_data[dps_code]['name'] + try: + scale = self.dps_data[dps_code]['scale'] + except KeyError: + scale = 1 + try: + unit = self.dps_data[dps_code]['unit'] + except KeyError: + unit = "" + val = status_data['dps'][dps_code] + if isinstance(val, int): + val = val / scale + return {name+'_raw': val, + name+'_fmt': str(val) + ' '+ unit} + + def get_forward_energy_total(self, status_data=None): + return self.get_value(dps_code=self.DPS_FORWARD_ENERGY_TOTAL) + + def get_reverse_energy_total(self, status_data=None): + return self.get_value(dps_code=self.DPS_REVERSE_ENERGY_TOTAL) + + def get_power_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_A) + + def get_dir_cur_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_DIR_CUR_A) + + def get_dir_cur_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_DIR_CUR_B) + + def get_power_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_B) + + def get_energy_forward_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_FORWARD_A) + + def get_energy_reverse_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_REVERSE_A) + + def get_energy_forward_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_FORWARD_B) + + def get_energy_reverse_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_REVERSE_B) + + def get_power_factor_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_FACTOR_A) + + def get_freq(self, status_data=None): + return self.get_value(dps_code=self.DPS_FREQ) + + def get_voltage(self, status_data=None): + return self.get_value(dps_code=self.DPS_VOLTAGE) + + def get_current_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_CURRENT_A) + + def get_current_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_CURRENT_B) + + def get_total_power(self, status_data=None): + return self.get_value(dps_code=self.DPS_TOTAL_POWER) + + def get_voltage_calibration(self, status_data=None): + return self.get_value(dps_code=self.DPS_VOLTAGE_CALIBRATION) + + def get_current_calibration_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_CURRENT_CALIBRATION_A) + + def get_power_calibration_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_CALIBRATION_A) + + def get_energy_calibration_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_CALIBRATION_A) + + def get_power_factor_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_FACTOR_B) + + def get_current_calibration_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_CURRENT_CALIBRATION_B) + + def get_power_calibration_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_POWER_CALIBRATION_B) + + def get_energy_calibration_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_CALIBRATION_B) + + def get_energy_reverse_calibration_a(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_CALIBRATION_REVERSE_A) + + def get_energy_reverse_calibration_b(self, status_data=None): + return self.get_value(dps_code=self.DPS_ENERGY_CALIBRATION_REVERSE_B) + + def get_report_rate(self, status_data=None): + return self.get_value(dps_code=self.DPS_REPORT_RATE) + + def print_all(self, status_data=None): + if status_data is None: + status_data = self.status() + print(self.get_forward_energy_total(status_data)) + print(self.get_reverse_energy_total(status_data)) + print(self.get_power_a(status_data)) + print(self.get_dir_cur_a(status_data)) + print(self.get_dir_cur_b(status_data)) + print(self.get_power_b(status_data)) + print(self.get_energy_forward_a(status_data)) + print(self.get_energy_reverse_a(status_data)) + print(self.get_energy_forward_b(status_data)) + print(self.get_energy_reverse_b(status_data)) + print(self.get_power_factor_a(status_data)) + print(self.get_freq(status_data)) + print(self.get_voltage(status_data)) + print(self.get_current_a(status_data)) + print(self.get_current_b(status_data)) + print(self.get_total_power(status_data)) + print(self.get_voltage_calibration(status_data)) + print(self.get_current_calibration_a(status_data)) + print(self.get_power_calibration_a(status_data)) + print(self.get_energy_calibration_a(status_data)) + print(self.get_power_factor_b(status_data)) + print(self.get_power_calibration_b(status_data)) + print(self.get_energy_calibration_b(status_data)) + print(self.get_energy_reverse_calibration_a(status_data)) + print(self.get_energy_reverse_calibration_b(status_data)) + print(self.get_report_rate(status_data)) +