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 WiFi Dual Meter device #569

Merged
merged 2 commits into from
Dec 18, 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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <ul><li>FORWARD</li><li>REVERSE</li></ul> ||
|104|Direction of current flow (B)| enum | <ul><li>FORWARD</li><li>REVERSE</li></ul> ||
|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)
```
ggardet marked this conversation as resolved.
Show resolved Hide resolved

### 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
Expand Down
22 changes: 22 additions & 0 deletions tinytuya/Contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
243 changes: 243 additions & 0 deletions tinytuya/Contrib/WiFiDualMeterDevice.py
Original file line number Diff line number Diff line change
@@ -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))

Loading