Skip to content

fix: TS011F dynamic multi-gang support and metering via electricityMeter#12606

Closed
hml96 wants to merge 1 commit into
Koenkk:masterfrom
hml96:ts011f-dynamic-multi-gang
Closed

fix: TS011F dynamic multi-gang support and metering via electricityMeter#12606
hml96 wants to merge 1 commit into
Koenkk:masterfrom
hml96:ts011f-dynamic-multi-gang

Conversation

@hml96

@hml96 hml96 commented Jul 2, 2026

Copy link
Copy Markdown
  • TS011F_plug_1: detect genOnOff endpoints at runtime; first gang stays unsuffixed "state" (single-gang devices unchanged), additional gangs exposed as CH with per-gang countdown. Metering moved to m.electricityMeter() (reads divisors from device, fallback values saved in configure). multiEndpointSkip keeps device-level attributes unsuffixed. String endpoint names avoid HA discovery capitalize crash.
  • TS011F_plug_3: remove version-only fingerprints (64/65/68/69/100/160) so TS011F devices without a specific fingerprint fall through to TS011F_plug_1.
  • Update tests: version-only fingerprint expectation now resolves to TS011F_plug_1; calibration/precision option order follows electricityMeter (voltage before current).

Link to picture pull request: TODO

@hml96

hml96 commented Jul 2, 2026

Copy link
Copy Markdown
Author

The Tuya socket definitions have become fragmented and inconsistent across many contributors. We are consolidating them under the main TS011F model definition, and removing the faulty fingerprint matches that rely on modelID + applicationVersion.

Comment thread src/devices/tuya.ts
{
fingerprint: [
{modelID: "TS011F", applicationVersion: 192, manufacturerName: "_TZ3000_2uollq9d", priority: -1},
{modelID: "TS011F", applicationVersion: 160, priority: -1},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will remove polling for those devices (and thus break electricity measurements). I think it's better to support multi-gang definitions with a separate definition.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fingerprint matching based on modelID + applicationVersion is unreliable by design. In Tuya's product line, metering sockets and power strips — whether single-gang or multi-gang, and regardless of the chip platform — all use TS011F as the model ID. The unified handling logic should therefore live in the single TS011F definition (TS011F_plug_1).

Matching on modelID + applicationVersion to route certain devices to a separate definition shouldn't exist in the first place: applicationVersion is not a stable product identifier — the same value can appear on completely unrelated products, and a firmware update can change it on the same product. When a specific device genuinely needs divergent logic, the correct way to single it out is modelID + manufacturerName.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with that, ideally go with modelId + manuf + appVersion, but that doesn't change that the appVersion 69 plugs are not polled anymore (which will break the energy reporting for those)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tuya smart sockets feature a scheduled automatic reporting function, so energy reporting capabilities are not completely lost.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A large number of other users are running the same appVersion, and we have received extensive feedback, so we believe it is necessary to correct this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The official firmware for Tuya sockets certainly includes a scheduled reporting function. If a device is observed failing to report, there are two possibilities: first, a failure in tuya.modernExtend.tuyaBase()—specifically, if the SDK fails to receive the necessary read command during network joining or power-up, it will disable the active reporting capability; second, a third-party developer using our SDK for custom development may have disabled the active reporting synchronization feature. However, we believe that in either case, this affects only a small number of devices and should not impact the majority.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extend: [tuya.modernExtend.tuyaBase(), m.identify(), m.electricityMeter()],
        fromZigbee: [
            fz.on_off,
            tuya.fz.on_off_countdown,
            tuya.fz.power_outage_memory,
            tuya.fz.child_lock,
            tuya.fz.indicator_mode,
            tuya.fz.switch_type_button,
        ],
        toZigbee: [
            tz.on_off,
            tuya.tz.on_off_countdown,
            tuya.tz.power_on_behavior_1,
            tuya.tz.child_lock,
            tuya.tz.backlight_indicator_mode_1,
            tuya.tz.switch_type_button,
        ],
        // Dynamic multi-gang: first endpoint stays as unsuffixed "state"; the rest become CH<ID>.
        // A single function controls ordering: state -> CH2..CHN -> features.
        exposes: (device) => {
            const eps = !utils.isDummyDevice(device) ? device.endpoints.filter((ep) => ep.supportsInputCluster("genOnOff")) : [];
            const mfr = device?.manufacturerName;
            const feats = [];
            feats.push(e.switch()); // first endpoint = state
            for (const ep of eps.slice(1)) feats.push(e.switch().withEndpoint("CH" + ep.ID)); // CH2..CHN
            // Features (mirrors the vendor-specific conditions of the original plug_1)
			feats.push(tuya.exposes.countdown().withAccess(ea.ALL)); // first endpoint -> countdown
			for (const ep of eps.slice(1))
				feats.push(
					tuya.exposes
						.countdown()
						.withAccess(ea.ALL)
						.withEndpoint("CH" + ep.ID),
				); // CH2..CHN -> countdown_CHn
            feats.push(tuya.exposes.powerOutageMemory());
            feats.push(tuya.exposes.indicatorMode());
            feats.push(e.child_lock());
            feats.push(tuya.exposes.switchTypeButton());
            return feats;
        },
        endpoint: (device) => {
            const map: {[s: string]: number} = {};
            const eps = device?.endpoints ? device.endpoints.filter((ep) => ep.supportsInputCluster("genOnOff")) : [];
            for (const ep of eps.slice(1)) map["CH" + ep.ID] = ep.ID;
            return map;
        },
        meta: {
            multiEndpoint: true,
            // Device-level attributes must not get an endpoint suffix.
            // Do NOT list "state" here: multi-gang switching relies on "state" being suffixed per endpoint.
            multiEndpointSkip: ["power", "current", "voltage", "energy", "child_lock", "power_outage_memory", "indicator_mode", "switch_type"],
        },
        configure: async (device, coordinatorEndpoint) => {
            await tuya.configureMagicPacket(device, coordinatorEndpoint);
            const endpoint = device.getEndpoint(1);
            await reporting.bind(endpoint, coordinatorEndpoint, ["genOnOff", "haElectricalMeasurement", "seMetering"]);

			await reporting.currentSummDelivered(endpoint);
			await reporting.rmsVoltage(endpoint, {change: 5});
			await reporting.rmsCurrent(endpoint, {change: 50});
            
            // Fallback scaling factors, used only when the device does not report its own
            // multipliers/divisors. m.electricityMeter() reads them from the device and,
            // when successful, overrides the values saved here.
            endpoint.saveClusterAttributeKeyValue("haElectricalMeasurement", {
                acVoltageMultiplier: 1,
                acVoltageDivisor: 1,
                acPowerMultiplier: 1,
                acPowerDivisor: 1,
                acCurrentDivisor: 1000,
                acCurrentMultiplier: 1,
            });
            endpoint.saveClusterAttributeKeyValue("seMetering", {
                divisor: 100,
                multiplier: 1,
            });
            utils.attachOutputCluster(device, "genOta");
            device.save();
        },
    },

Alternatively, I could synchronize this logic with the existing modelID + applicationVersion matching logic without disrupting the original polling mechanism; do you think that would be feasible?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first, a failure in tuya.modernExtend.tuyaBase()—specifically, if the SDK fails to receive the necessary read command during network joining or power-up

For my device the configure passes, (we call it the "tuya magic packet"), so this cannot be the case.

a third-party developer using our SDK for custom development may have disabled the active reporting synchronization feature

I bought from AliExpress, I guess that should come with the official fw?

However, we believe that in either case, this affects only a small number of devices and should not impact the majority.

I think quite some devices will be impacted (given the list of fingerprints that have polling enabled). For all of these devices, polling was enabled after we figured out they do not report values themselves.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help me review the new submission?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the polling is still not present, it would be good if you could provide an external converter such I can test if it works with my plug without polling?

Add a new TS011F_Socket definition matched by manufacturerName for
multi-gang power monitoring sockets. Detects genOnOff endpoints at
runtime: first endpoint stays unsuffixed (state), additional endpoints
become CH2..CHN. Metering via m.electricityMeter(). Tuya-specific
features (countdown per gang, power-outage memory, indicator mode,
child lock, switch type) included.

Restore the version-only fingerprints in TS011F_plug_3 (previously
removed) with priority:-1 so they remain lower priority than any
manufacturerName-based fingerprint (priority 0).
@hml96 hml96 force-pushed the ts011f-dynamic-multi-gang branch from 0a081a3 to f34629b Compare July 8, 2026 06:15
@hml96

hml96 commented Jul 8, 2026

Copy link
Copy Markdown
Author

I have made further revisions; please review it again.

@hml96 hml96 closed this Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants