Skip to content

fix(IRRemoteControlDevice): raise RuntimeError on undetected control_type in send_command()#698

Merged
jasonacox merged 3 commits into
jasonacox:masterfrom
jasonacox-sam:fix/ir-control-type-detection-and-silent-failure
Apr 11, 2026
Merged

fix(IRRemoteControlDevice): raise RuntimeError on undetected control_type in send_command()#698
jasonacox merged 3 commits into
jasonacox:masterfrom
jasonacox-sam:fix/ir-control-type-detection-and-silent-failure

Conversation

@jasonacox-sam
Copy link
Copy Markdown
Collaborator

@jasonacox-sam jasonacox-sam commented Apr 11, 2026

Summary

send_command() would silently no-op if detect_control_type() failed to identify the device type — leaving control_type=0 with no error, no log output, and no IR signals sent. This made the failure very difficult to diagnose.

Surfaced in issue #492 where a user spent considerable time debugging before finding a workaround. (Note: the root cause in #492 was a leading 0 in key1, not a detection failure — but the silent no-op behavior is worth fixing regardless.)

Changes

detect_control_type() — cleaner failure logging

Removed the empty-DPS fallback and the detection-failure default that silently set control_type=1. If detection fails, control_type stays 0 and a warning is logged. Users can set control_type manually.

send_command() — raise on undetected control_type

If send_command() is called with control_type=0, raise a clear RuntimeError with an actionable message instead of silently dropping the command. This surfaces the failure immediately and tells the user exactly what to do.

send_command() — fix mutable default argument

Changed data={} default argument to data=None with an explicit guard inside the function, avoiding the Python mutable default argument pitfall.

Review Notes

Per @uzlonewolf: blindly defaulting control_type=1 on empty DPS would mask real detection failures. The RuntimeError in send_command() is the right place to catch undetected control types.

Related Issues

Related to #492

…tected control_type

Some IR devices (e.g. Aubess IR PRO, product ID wnykq) return an empty
dps {} from status() instead of advertising their supported DPs. The
previous detection logic would silently leave control_type=0, and
send_command() would then silently no-op — making it very hard to
diagnose why no IR signals were being sent.

Two fixes:

1. detect_control_type(): When status() returns an empty dps dict,
   fall back to control_type=1 (DPS 201) rather than giving up.
   DPS 201 is the original/most common IR control DP and is a safe
   default for devices that don't advertise their DPs.  Also change
   the detection failure path from 'leave at 0' to 'default to 1'
   with an updated warning message.

2. send_command(): Raise a clear RuntimeError when control_type is
   still 0 instead of silently dropping the command.  This surfaces
   the problem immediately with an actionable message.

Fixes issue jasonacox#492 (Aubess WiFi IR Controller S16 - cannot control device
via IRRemoteControlDevice Contrib class when dps {} is empty).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves IRRemoteControlDevice robustness for IR controllers that return an empty dps {} from status(), avoiding silent no-op behavior and making failures more diagnosable for users (e.g., Aubess IR PRO / issue #492).

Changes:

  • Add an empty-dps fallback in detect_control_type() to default to control_type=1 (DPS 201).
  • Default to control_type=1 when detection fails, instead of leaving control_type=0.
  • Raise a clear RuntimeError in send_command() if control_type is still 0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

self.set_socketTimeout( old_timeout )
self.set_socketPersistent( old_persist )

def send_command( self, mode, data={} ):
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

send_command() uses a mutable default argument (data={}), which can lead to shared state across calls if the dict is ever mutated (directly or indirectly). Prefer data=None and assign {} inside the function to avoid this class of bugs.

Suggested change
def send_command( self, mode, data={} ):
def send_command( self, mode, data=None ):
if data is None:
data = {}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in the latest commit (bf1c0b8). Changed data={} to data=None with an explicit if data is None: data = {} guard inside the function. Agreed this is the right pattern.

Comment on lines +186 to +205
log.warning( 'Detect control type failed! control_type= must be set manually' )
log.warning( 'Detect control type failed! Defaulting to control_type=1 (DPS 201). '
'Override with control_type=1 or control_type=2 if needed.' )
self.control_type = 1
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

New control-type detection behavior (empty-dps fallback and default-to-1 on detection failure) is not covered by unit tests. Since the repo already has unittest coverage for RFRemoteControlDevice in tests.py, please add similar mocked tests for IRRemoteControlDevice.detect_control_type() to lock in the empty-dps path and the failure-default path.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed that tests for the new detect_control_type() paths would be valuable. That said, I'd prefer to keep this PR focused on the robustness fix itself — the existing test suite doesn't have IRRemoteControlDevice coverage at all, so adding it properly is a meaningful effort that deserves its own PR. I'll open a follow-up specifically for IR test coverage.

Comment on lines +210 to +215
if not self.control_type:
raise RuntimeError(
'IRRemoteControlDevice: control_type has not been detected. '
'Pass control_type=1 (DPS 201/202) or control_type=2 (DPS 1-13) '
'to the constructor, or call detect_control_type() first.'
)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The new RuntimeError path in send_command() should be exercised by a unit test (e.g., force control_type=0 and assert the exception/message) so this behavior doesn’t regress back to a silent no-op.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same as above — the RuntimeError path in send_command() is worth locking in with a test, but I'd rather not expand this PR's scope. Will include it in the IR test coverage follow-up PR.

@jasonacox-sam
Copy link
Copy Markdown
Collaborator Author

@ClusterM — since you created the IRRemoteControlDevice module, we would welcome your feedback if you care to comment. This PR addresses an issue where devices like the Aubess IR PRO return an empty dps {} on status(), causing silent failures. The fix defaults control_type to 1 (DPS 201) in those cases and raises a clear RuntimeError if detection is still unresolved.

@uzlonewolf @jasonacox — if this looks good to you both, it should be ready to merge. Will plan to merge in ~1 week if no objections.

@uzlonewolf
Copy link
Copy Markdown
Collaborator

I disagree with blindly setting control_type to 1, this is just going to cause weird problems later. The only part of this PR we should keep is the RuntimeError when a send is attempted with control_type=0.

I see no evidence that the detection failed in #492 - his problem was that the leading '0' was not removed from key1 when switching from rawdogging the JSON to using the Contrib module.

@jasonacox-sam
Copy link
Copy Markdown
Collaborator Author

jasonacox-sam commented Apr 11, 2026

@uzlonewolf — thanks for the quick review. Let me explain the reasoning behind the empty-DPS fallback before we decide what to do with it.

The original issue in #492 showed a device (Aubess IR PRO) that returns an empty dps {} from status(). When detect_control_type() encounters that, neither the DP_MODE branch nor the DPS-key branches match, so control_type stays 0. Then any call to send_command() silently does nothing — no error, no log message, just a quiet failure that's extremely hard to debug.

My reasoning for defaulting to control_type=1 in that case was: DPS 201 is the more common IR command set, and a device that can't tell us its type is more likely to respond to DPS 201 than DPS 1-13. It's a best-effort fallback rather than a guaranteed answer.

That said, your point has merit: "best-effort guess" can mask real detection failures and cause confusing behavior for devices where the guess is wrong. A failed detection that defaults silently to type 1 is arguably worse than a failed detection that fails loudly — at least the loud failure tells the user something is actually wrong.

Where I think we agree: the RuntimeError in send_command() when control_type=0 is unambiguously useful regardless — it turns a silent no-op into a diagnosable error.

So the question is really: should we keep the empty-DPS fallback alongside the RuntimeError, or drop it and let the RuntimeError do all the work? I can see the argument for dropping it — if detection genuinely fails, the RuntimeError will catch it, and the user gets a clear message to set control_type manually. That's cleaner than a silent default.

Happy to revise the PR to remove the empty-DPS fallback if that's the direction you prefer. What do you think?

@uzlonewolf
Copy link
Copy Markdown
Collaborator

@jasonacox-sam Yes, that's right.

@jasonacox-sam
Copy link
Copy Markdown
Collaborator Author

@uzlonewolf — updated in 9ea07d0. Removed the empty-DPS fallback and the detection-failure default entirely. detect_control_type() now logs a warning and leaves control_type=0 if detection fails, and send_command() raises a RuntimeError if called in that state. Thanks for the guidance.

@jasonacox
Copy link
Copy Markdown
Owner

Agree. @jasonacox-sam I recommend you update the PR Summary and Changes note to correctly reflect the revised change before merge.

@jasonacox-sam jasonacox-sam changed the title fix(IRRemoteControlDevice): handle empty DPS on detect, raise on undetected control_type fix(IRRemoteControlDevice): raise RuntimeError on undetected control_type in send_command() Apr 11, 2026
@jasonacox-sam
Copy link
Copy Markdown
Collaborator Author

@jasonacox — PR title and description updated to reflect the revised change. Ready for merge when you are.

@jasonacox
Copy link
Copy Markdown
Owner

Thanks @jasonacox-sam - good contribution. And thanks @uzlonewolf 🙏

Merging.

@jasonacox jasonacox merged commit da2a5b7 into jasonacox:master Apr 11, 2026
17 checks passed
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.

4 participants