Panasonic Klimasteuerung mit Intesis PA-AC-WMP-1

Begonnen von Wassilis, 01 Januar 2026, 17:09:45

Vorheriges Thema - Nächstes Thema

Wassilis

Hallo zusammen,

ich hatte vor einiger Zeit ein Modul gefunden, welches mir erlaubt über ein externes WLAN Modul meiner Klimaanlage die Steuerung über FHEM zu organisieren. Leider finde ich dieses Modul nicht mehr und ein Backup ist auf der Festplatte die gerade heute den Geist aufgegeben hat. Ich vermute es basierte auf dem HV_AC_Daikin oder dem HTTPMOD. Daikin klappt aber nicht weil wohl Port 80 acuh nicht mehr geht.

Damals hatte ich das so definiert:

define Klima2 Klimaanlage 192.168.xx.xx
setuuid Klima2 5ee4609f-f33f-7a39-762f-887d9bb94f209dd2
attr Klima2 devStateIcon OFF.*:control_standby@gray:ON ON/COOL.*:frost@blue:OFF ON/HEAT.*:sani_heating@red:OFF ON/DRY.*:humidity@blue:OFF ON/FAN.*:vent_ventilation@green:OFF ON/AUTO.*:temp_temperature@red:OFF
attr Klima2 interval 180
attr Klima2 interval_powered 180
attr Klima2 room Wohnzimmer
attr Klima2 stateFormat ONOFF/MODE\
<br/>In: AMBTEMP &degC <br/>Fan Speed: FANSP
define SVG_FileLog_Klima2_1 SVG FileLog_Klima2:SVG_FileLog_Klima2_1:CURRENT
setuuid SVG_FileLog_Klima2_1 5ee46d4e-f33f-7a39-9939-c993fd8fb896483e

Ich meine ich hatte Klimaanlage in der 98_myUtils.pm untergebracht, aber ist schon 5 Jahre her.
Hat da jemand was parat... ?

Geht um dieses Gerät:
https://www.hms-networks.com/p/inwmppan001i000-panasonic-etherea-ac-units-to-home-automation-interface


Wassilis

Habe es mal über KI gelöst und ein Modul über KI schreiben lassen. Ergebnis kann sich sehen lassen. Funktioniert sehr gut.

##############################################
# $Id: 98_IntesisWMP.pm 2025-01-01 $
#
# FHEM Module for Intesis WMP Protocol (PA-AC-WMP-1 / INWMPPAN001I000)
# Controls air conditioning devices via TCP/ASCII protocol on port 3310
#
# Based on WMP Protocol Specification V1.11
#
package main;

use strict;
use warnings;
use IO::Socket::INET;
use Time::HiRes qw(gettimeofday);

# Module version
my $IntesisWMP_Version = "1.2.2";

# Constants
my $WMP_PORT = 3310;
my $WMP_TIMEOUT = 5;
my $WMP_KEEPALIVE_INTERVAL = 30;    # seconds between pings (must be < 60s)
my $WMP_UPDATE_INTERVAL = 60;      # seconds between full updates

##############################################
# Initialize module
##############################################
sub IntesisWMP_Initialize($) {
    my ($hash) = @_;

    $hash->{DefFn}      = "IntesisWMP_Define";
    $hash->{UndefFn}    = "IntesisWMP_Undef";
    $hash->{SetFn}      = "IntesisWMP_Set";
    $hash->{GetFn}      = "IntesisWMP_Get";
    $hash->{ReadFn}    = "IntesisWMP_Read";
    $hash->{ReadyFn}    = "IntesisWMP_Ready";
    $hash->{AttrFn}    = "IntesisWMP_Attr";
    $hash->{NotifyFn}  = "IntesisWMP_Notify";
   
    $hash->{AttrList} =
        "disable:0,1 " .
        "interval " .
        "updateInterval " .
        "timeout " .
        "username " .
        "password " .
        "unit " .
        "tempUnit:C,F " .
        "pollFunctions " .
        $readingFnAttributes;

    Log3 undef, 3, "IntesisWMP: Module initialized (Version $IntesisWMP_Version)";
}

##############################################
# Define device
##############################################
sub IntesisWMP_Define($$) {
    my ($hash, $def) = @_;
    my @a = split("[ \t][ \t]*", $def);

    return "Usage: define <name> IntesisWMP <host> [<port>]" if(@a < 3);

    my $name = $a[0];
    my $host = $a[2];
    my $port = $a[3] || $WMP_PORT;

    $hash->{HOST} = $host;
    $hash->{PORT} = $port;
    $hash->{UNIT} = 1;  # Default unit number
    $hash->{INTERVAL} = $WMP_KEEPALIVE_INTERVAL;
    $hash->{UPDATE_INTERVAL} = $WMP_UPDATE_INTERVAL;
    $hash->{TIMEOUT} = $WMP_TIMEOUT;
    $hash->{VERSION} = $IntesisWMP_Version;
    $hash->{NOTIFYDEV} = "global";
   
    # Initialize state
    $hash->{STATE} = "Initialized";
    $hash->{LAST_RECV} = 0;
    $hash->{LAST_SEND} = 0;
    $hash->{LAST_UPDATE} = 0;
    $hash->{CONNECTED} = 0;
    $hash->{BUFFER} = "";
   
    # Valid values for readings/settings
    $hash->{helper}{validModes} = {
        AUTO => 1, HEAT => 1, DRY => 1, FAN => 1, COOL => 1
    };
    $hash->{helper}{validFanSpeeds} = {
        AUTO => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1,
        6 => 1, 7 => 1, 8 => 1, 9 => 1
    };
    $hash->{helper}{validVanes} = {
        AUTO => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1,
        6 => 1, 7 => 1, 8 => 1, 9 => 1, SWING => 1
    };
   
    # Default poll functions
    $hash->{helper}{pollFunctions} = [qw(ONOFF MODE SETPTEMP AMBTEMP FANSP VANEUD VANELR)];

    Log3 $name, 3, "IntesisWMP ($name): Defined with host $host:$port";
   
    # Start connection after FHEM is initialized
    if($init_done) {
        IntesisWMP_Connect($hash);
    }
   
    return undef;
}

##############################################
# Handle FHEM notifications (global events)
##############################################
sub IntesisWMP_Notify($$) {
    my ($hash, $dev) = @_;
    my $name = $hash->{NAME};
   
    return if(AttrVal($name, "disable", 0));
   
    if($dev->{NAME} eq "global") {
        my $events = deviceEvents($dev, 1);
        return if(!$events);
       
        foreach my $event (@{$events}) {
            if($event =~ /^INITIALIZED$/) {
                Log3 $name, 4, "IntesisWMP ($name): FHEM initialized, connecting...";
                IntesisWMP_Connect($hash);
            }
        }
    }
    return undef;
}

##############################################
# Connect to the Intesis device
##############################################
sub IntesisWMP_Connect($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    return if(AttrVal($name, "disable", 0));
   
    # Close existing connection if any
    IntesisWMP_Disconnect($hash);
   
    my $host = $hash->{HOST};
    my $port = $hash->{PORT};
    my $timeout = $hash->{TIMEOUT};
   
    Log3 $name, 3, "IntesisWMP ($name): Connecting to $host:$port...";
   
    # Create non-blocking connection
    my $conn = IO::Socket::INET->new(
        PeerAddr => $host,
        PeerPort => $port,
        Proto    => 'tcp',
        Timeout  => $timeout,
        Blocking => 0
    );
   
    if(!$conn) {
        Log3 $name, 2, "IntesisWMP ($name): Connection failed: $!";
        $hash->{STATE} = "disconnected";
        $hash->{CONNECTED} = 0;
        readingsSingleUpdate($hash, "state", "disconnected", 1);
       
        # Schedule reconnect
        RemoveInternalTimer($hash, "IntesisWMP_Connect");
        InternalTimer(gettimeofday() + 60, "IntesisWMP_Connect", $hash, 0);
        return;
    }
   
    # Store connection handle
    $hash->{FD} = $conn->fileno();
    $hash->{CD} = $conn;
    $hash->{CONNECTED} = 1;
    $hash->{STATE} = "connected";
    $hash->{BUFFER} = "";
    $hash->{LAST_RECV} = gettimeofday();
   
    $selectlist{$name} = $hash;
   
    Log3 $name, 3, "IntesisWMP ($name): Connected to $host:$port";
    readingsSingleUpdate($hash, "state", "connected", 1);
   
    # Login if credentials are set
    my $username = AttrVal($name, "username", "");
    my $password = AttrVal($name, "password", "");
    if($username && $password) {
        IntesisWMP_Login($hash, $username, $password);
    }
   
    # Request device info
    IntesisWMP_Send($hash, "ID");
   
    # Start keepalive timer
    IntesisWMP_StartKeepaliveTimer($hash);
   
    # Start update timer (initial update after 2 seconds)
    IntesisWMP_StartUpdateTimer($hash, 2);
   
    return;
}

##############################################
# Start keepalive timer
##############################################
sub IntesisWMP_StartKeepaliveTimer($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    RemoveInternalTimer($hash, "IntesisWMP_Keepalive");
    my $interval = AttrVal($name, "interval", $hash->{INTERVAL});
    Log3 $name, 4, "IntesisWMP ($name): Starting keepalive timer (interval: ${interval}s)";
    InternalTimer(gettimeofday() + $interval, "IntesisWMP_Keepalive", $hash, 0);
}

##############################################
# Start update timer
##############################################
sub IntesisWMP_StartUpdateTimer($;$) {
    my ($hash, $delay) = @_;
    my $name = $hash->{NAME};
   
    RemoveInternalTimer($hash, "IntesisWMP_UpdateTimer");
    my $interval = AttrVal($name, "updateInterval", $hash->{UPDATE_INTERVAL});
    $delay = $interval if(!defined($delay));
   
    Log3 $name, 4, "IntesisWMP ($name): Starting update timer (delay: ${delay}s, interval: ${interval}s)";
    InternalTimer(gettimeofday() + $delay, "IntesisWMP_UpdateTimer", $hash, 0);
}

##############################################
# Disconnect from device
##############################################
sub IntesisWMP_Disconnect($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    Log3 $name, 4, "IntesisWMP ($name): Disconnecting, removing all timers...";
   
    RemoveInternalTimer($hash, "IntesisWMP_Keepalive");
    RemoveInternalTimer($hash, "IntesisWMP_Connect");
    RemoveInternalTimer($hash, "IntesisWMP_UpdateTimer");
   
    # Remove staggered poll timers
    foreach my $func (qw(ONOFF MODE SETPTEMP AMBTEMP FANSP VANEUD VANELR ERRSTATUS ERRCODE)) {
        RemoveInternalTimer("$name:poll:$func");
    }
   
    if($hash->{CD}) {
        Log3 $name, 4, "IntesisWMP ($name): Closing connection...";
        delete $selectlist{$name};
        close($hash->{CD});
        delete $hash->{CD};
        delete $hash->{FD};
    }
   
    $hash->{CONNECTED} = 0;
    $hash->{STATE} = "disconnected";
   
    return;
}

##############################################
# Undefine device
##############################################
sub IntesisWMP_Undef($$) {
    my ($hash, $arg) = @_;
    my $name = $hash->{NAME};
   
    IntesisWMP_Disconnect($hash);
   
    Log3 $name, 3, "IntesisWMP ($name): Device undefined";
    return undef;
}

##############################################
# Send command to device
##############################################
sub IntesisWMP_Send($$) {
    my ($hash, $cmd) = @_;
    my $name = $hash->{NAME};
   
    if(!$hash->{CD} || !$hash->{CONNECTED}) {
        Log3 $name, 3, "IntesisWMP ($name): Not connected, cannot send: $cmd";
        IntesisWMP_Connect($hash);
        return "Not connected";
    }
   
    my $msg = $cmd . "\r\n";
   
    Log3 $name, 4, "IntesisWMP ($name): Sending: $cmd";
   
    my $written = syswrite($hash->{CD}, $msg);
    if(!defined($written)) {
        Log3 $name, 2, "IntesisWMP ($name): Write error: $!";
        IntesisWMP_Disconnect($hash);
        readingsSingleUpdate($hash, "state", "disconnected", 1);
        IntesisWMP_Connect($hash);
        return "Write error";
    }
   
    $hash->{LAST_SEND} = gettimeofday();
    return undef;
}

##############################################
# Read data from device (called by select loop)
##############################################
sub IntesisWMP_Read($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    my $buf;
    my $len = sysread($hash->{CD}, $buf, 1024);
   
    if(!defined($len) || $len <= 0) {
        Log3 $name, 3, "IntesisWMP ($name): Connection closed by remote";
        IntesisWMP_Disconnect($hash);
        readingsSingleUpdate($hash, "state", "disconnected", 1);
        # Schedule reconnect
        InternalTimer(gettimeofday() + 10, "IntesisWMP_Connect", $hash, 0);
        return;
    }
   
    $hash->{LAST_RECV} = gettimeofday();
    $hash->{BUFFER} .= $buf;
   
    # Process complete lines
    while($hash->{BUFFER} =~ s/^([^\r\n]*)\r?\n//) {
        my $line = $1;
        next if($line eq "");
       
        Log3 $name, 4, "IntesisWMP ($name): Received: $line";
        IntesisWMP_Parse($hash, $line);
    }
   
    return;
}

##############################################
# Ready function for reconnect
##############################################
sub IntesisWMP_Ready($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    return if(AttrVal($name, "disable", 0));
   
    if(!$hash->{CD} || !$hash->{CONNECTED}) {
        return IntesisWMP_Connect($hash);
    }
   
    return 0;
}

##############################################
# Parse received data
##############################################
sub IntesisWMP_Parse($$) {
    my ($hash, $line) = @_;
    my $name = $hash->{NAME};
   
    $line = uc($line);  # Protocol is case-insensitive
   
    # ACK response
    if($line eq "ACK") {
        Log3 $name, 5, "IntesisWMP ($name): Command acknowledged";
        return;
    }
   
    # ERR response
    if($line eq "ERR") {
        Log3 $name, 3, "IntesisWMP ($name): Error response received";
        readingsSingleUpdate($hash, "lastError", "ERR", 1);
        return;
    }
   
    # ID response: Model, MAC, IP, Protocol, Version, RSSI, DeviceName, SecurityLevel, Generation
    if($line =~ /^ID:\s*(.+)/) {
        my @parts = split(/,/, $1);
        readingsBeginUpdate($hash);
        readingsBulkUpdate($hash, "model", IntesisWMP_trim($parts[0])) if(defined $parts[0]);
        readingsBulkUpdate($hash, "mac", IntesisWMP_trim($parts[1])) if(defined $parts[1]);
        readingsBulkUpdate($hash, "ip", IntesisWMP_trim($parts[2])) if(defined $parts[2]);
        readingsBulkUpdate($hash, "protocol", IntesisWMP_trim($parts[3])) if(defined $parts[3]);
        readingsBulkUpdate($hash, "firmware", IntesisWMP_trim($parts[4])) if(defined $parts[4]);
        readingsBulkUpdate($hash, "rssi", IntesisWMP_trim($parts[5])) if(defined $parts[5]);
        readingsBulkUpdate($hash, "deviceName", IntesisWMP_trim($parts[6])) if(defined $parts[6]);
        readingsBulkUpdate($hash, "securityLevel", IntesisWMP_trim($parts[7])) if(defined $parts[7]);
        readingsEndUpdate($hash, 1);
        return;
    }
   
    # INFO response: Function, Unit, InstallerId, Value
    if($line =~ /^INFO:\s*(\w+),\s*(\d+),\s*(\w+),\s*(.+)/) {
        my ($func, $unit, $installer, $value) = ($1, $2, $3, IntesisWMP_trim($4));
        IntesisWMP_UpdateReading($hash, $func, $value);
        return;
    }
   
    # CHN (spontaneous change) or GET response: Unit:Function,Value
    if($line =~ /^(CHN|GET),\s*(\d+):(\w+),\s*(.+)/) {
        my ($cmd, $unit, $func, $value) = ($1, $2, $3, IntesisWMP_trim($4));
        Log3 $name, 4, "IntesisWMP ($name): Parsed $cmd response: unit=$unit, func=$func, value=$value";
        IntesisWMP_UpdateReading($hash, $func, $value);
        return;
    }
   
    # Alternative CHN/GET format
    if($line =~ /^(\w+),\s*(\d+):(\w+),\s*(.+)/) {
        my ($cmd, $unit, $func, $value) = ($1, $2, $3, IntesisWMP_trim($4));
        if($cmd eq "CHN" || $cmd eq "GET" || $cmd eq "SET") {
            IntesisWMP_UpdateReading($hash, $func, $value);
        }
        return;
    }
   
    # LIMITS response
    if($line =~ /^LIMITS:\s*(\w+),\s*\[(.+)\]/) {
        my ($func, $values) = ($1, $2);
        readingsSingleUpdate($hash, "limits_" . lc($func), $values, 1);
        return;
    }
   
    # LOGIN response
    if($line =~ /^LOGIN:\s*(.+)/) {
        my $response = IntesisWMP_trim($1);
        if($response eq "OK") {
            Log3 $name, 3, "IntesisWMP ($name): Login successful";
            readingsSingleUpdate($hash, "login", "ok", 1);
        } else {
            Log3 $name, 2, "IntesisWMP ($name): Login failed: $response";
            readingsSingleUpdate($hash, "login", "failed", 1);
        }
        return;
    }
   
    # DISCOVER response (usually via UDP, but handle anyway)
    if($line =~ /^DISCOVER/ || $line =~ /^INWMP/) {
        Log3 $name, 4, "IntesisWMP ($name): Discover response: $line";
        return;
    }
   
    # PING response
    if($line eq "PONG") {
        Log3 $name, 5, "IntesisWMP ($name): Pong received";
        return;
    }
   
    Log3 $name, 4, "IntesisWMP ($name): Unhandled message: $line";
}

##############################################
# Update a reading based on function name
##############################################
sub IntesisWMP_UpdateReading($$$) {
    my ($hash, $func, $value) = @_;
    my $name = $hash->{NAME};
   
    $func = uc($func);
    $value = uc($value) unless($value =~ /^-?\d+$/);
   
    Log3 $name, 4, "IntesisWMP ($name): Updating reading for $func = $value";
   
    readingsBeginUpdate($hash);
   
    if($func eq "ONOFF") {
        readingsBulkUpdate($hash, "power", lc($value), 1);
        # Update state based on power and mode
        my $mode = ReadingsVal($name, "mode", "");
        if($value eq "ON" && $mode) {
            readingsBulkUpdate($hash, "state", lc($mode), 1);
        } else {
            readingsBulkUpdate($hash, "state", lc($value), 1);
        }
    }
    elsif($func eq "MODE") {
        readingsBulkUpdate($hash, "mode", lc($value), 1);
        my $power = ReadingsVal($name, "power", "off");
        if($power eq "on") {
            readingsBulkUpdate($hash, "state", lc($value), 1);
        }
    }
    elsif($func eq "SETPTEMP") {
        # Temperature is multiplied by 10
        my $temp = $value / 10.0;
        readingsBulkUpdate($hash, "desiredTemperature", $temp, 1);
    }
    elsif($func eq "AMBTEMP") {
        # Ambient temperature is multiplied by 10
        my $temp = $value / 10.0;
        Log3 $name, 3, "IntesisWMP ($name): AMBTEMP received: raw=$value, converted=$temp";
        readingsBulkUpdate($hash, "temperature", $temp, 1);
    }
    elsif($func eq "FANSP") {
        readingsBulkUpdate($hash, "fanSpeed", lc($value), 1);
    }
    elsif($func eq "VANEUD") {
        readingsBulkUpdate($hash, "vaneVertical", lc($value), 1);
    }
    elsif($func eq "VANELR") {
        readingsBulkUpdate($hash, "vaneHorizontal", lc($value), 1);
    }
    elsif($func eq "ERRSTATUS") {
        readingsBulkUpdate($hash, "errorStatus", $value, 1);
    }
    elsif($func eq "ERRCODE") {
        readingsBulkUpdate($hash, "errorCode", $value, 1);
    }
    else {
        readingsBulkUpdate($hash, lc($func), $value, 1);
    }
   
    readingsEndUpdate($hash, 1);
}

##############################################
# Keepalive timer - sends PING to prevent timeout
##############################################
sub IntesisWMP_Keepalive($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    return if(AttrVal($name, "disable", 0));
   
    if($hash->{CONNECTED} && $hash->{CD}) {
        Log3 $name, 5, "IntesisWMP ($name): Sending keepalive PING";
        IntesisWMP_Send($hash, "PING");
       
        # Schedule next keepalive
        IntesisWMP_StartKeepaliveTimer($hash);
    } else {
        Log3 $name, 4, "IntesisWMP ($name): Keepalive skipped - not connected";
    }
   
    return;
}

##############################################
# Update timer - periodically polls all values
##############################################
sub IntesisWMP_UpdateTimer($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
   
    return if(AttrVal($name, "disable", 0));
   
    Log3 $name, 3, "IntesisWMP ($name): Update timer triggered, CONNECTED=$hash->{CONNECTED}";
   
    if($hash->{CONNECTED} && $hash->{CD}) {
        Log3 $name, 3, "IntesisWMP ($name): Executing periodic update - polling all functions";
       
        # Poll all functions
        IntesisWMP_GetAll($hash);
       
        $hash->{LAST_UPDATE} = gettimeofday();
        readingsSingleUpdate($hash, ".lastUpdate", scalar(localtime()), 0);
       
        # Schedule next update
        IntesisWMP_StartUpdateTimer($hash);
    } else {
        Log3 $name, 3, "IntesisWMP ($name): Update timer - not connected, skipping poll";
        # If not connected, try to reconnect
        IntesisWMP_Connect($hash);
    }
   
    return;
}

##############################################
# Get all current values from device
# Uses staggered timing to avoid overwhelming the device
##############################################
sub IntesisWMP_GetAll($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
    my $unit = AttrVal($name, "unit", $hash->{UNIT});
   
    # Get poll functions from attribute or use default
    my $pollFuncsAttr = AttrVal($name, "pollFunctions", "");
    my @functions;
   
    if($pollFuncsAttr) {
        @functions = split(/[,\s]+/, uc($pollFuncsAttr));
    } else {
        @functions = @{$hash->{helper}{pollFunctions}};
    }
   
    Log3 $name, 3, "IntesisWMP ($name): Polling " . scalar(@functions) . " functions with staggered timing";
   
    # Send commands with 0.5 second delays to avoid overwhelming the device
    my $delay = 0;
    foreach my $func (@functions) {
        my $cmd = "GET,$unit:$func";
        if($delay == 0) {
            # Send first command immediately
            IntesisWMP_Send($hash, $cmd);
        } else {
            # Schedule subsequent commands with delays
            InternalTimer(gettimeofday() + $delay, sub {
                IntesisWMP_Send($hash, $cmd) if($hash->{CONNECTED});
            }, "$name:poll:$func", 0);
        }
        $delay += 0.5;  # 500ms between each command
    }
   
    return;
}

##############################################
# Login to device (if security is enabled)
##############################################
sub IntesisWMP_Login($$$) {
    my ($hash, $username, $password) = @_;
    my $name = $hash->{NAME};
   
    Log3 $name, 4, "IntesisWMP ($name): Logging in...";
    IntesisWMP_Send($hash, "LOGIN,$username,$password");
   
    return;
}

##############################################
# Set commands
##############################################
sub IntesisWMP_Set($@) {
    my ($hash, @a) = @_;
    my $name = shift @a;
    my $cmd = shift @a;
    my $value = join(" ", @a);
   
    return "\"set $name\" needs at least one argument" if(!defined($cmd));
   
    my $unit = AttrVal($name, "unit", $hash->{UNIT});
   
    # Build list of valid set commands
    my $setList = "on:noArg off:noArg " .
                  "mode:auto,heat,dry,fan,cool " .
                  "desiredTemperature:slider,16,0.5,32,1 " .
                  "fanSpeed:auto,1,2,3,4,5,6,7,8,9 " .
                  "vaneVertical:auto,1,2,3,4,5,swing " .
                  "vaneHorizontal:auto,1,2,3,4,5,swing " .
                  "reconnect:noArg " .
                  "statusRequest:noArg " .
                  "raw";
   
    if($cmd eq "on") {
        return IntesisWMP_Send($hash, "SET,$unit:ONOFF,ON");
    }
    elsif($cmd eq "off") {
        return IntesisWMP_Send($hash, "SET,$unit:ONOFF,OFF");
    }
    elsif($cmd eq "mode") {
        $value = uc($value);
        if(!$hash->{helper}{validModes}{$value}) {
            return "Invalid mode. Valid modes: auto, heat, dry, fan, cool";
        }
        return IntesisWMP_Send($hash, "SET,$unit:MODE,$value");
    }
    elsif($cmd eq "desiredTemperature" || $cmd eq "setptemp") {
        # Convert temperature to protocol format (multiply by 10)
        if($value !~ /^[\d.]+$/) {
            return "Invalid temperature value";
        }
        my $temp = int($value * 10);
        return IntesisWMP_Send($hash, "SET,$unit:SETPTEMP,$temp");
    }
    elsif($cmd eq "fanSpeed" || $cmd eq "fanspeed") {
        $value = uc($value);
        if(!$hash->{helper}{validFanSpeeds}{$value}) {
            return "Invalid fan speed. Valid values: auto, 1-9";
        }
        return IntesisWMP_Send($hash, "SET,$unit:FANSP,$value");
    }
    elsif($cmd eq "vaneVertical" || $cmd eq "vaneud") {
        $value = uc($value);
        if(!$hash->{helper}{validVanes}{$value}) {
            return "Invalid vane position. Valid values: auto, 1-9, swing";
        }
        return IntesisWMP_Send($hash, "SET,$unit:VANEUD,$value");
    }
    elsif($cmd eq "vaneHorizontal" || $cmd eq "vanelr") {
        $value = uc($value);
        if(!$hash->{helper}{validVanes}{$value}) {
            return "Invalid vane position. Valid values: auto, 1-9, swing";
        }
        return IntesisWMP_Send($hash, "SET,$unit:VANELR,$value");
    }
    elsif($cmd eq "reconnect") {
        IntesisWMP_Disconnect($hash);
        return IntesisWMP_Connect($hash);
    }
    elsif($cmd eq "statusRequest") {
        Log3 $name, 3, "IntesisWMP ($name): Manual status request triggered";
        IntesisWMP_GetAll($hash);
        return undef;
    }
    elsif($cmd eq "raw") {
        return IntesisWMP_Send($hash, $value);
    }
   
    return "Unknown command $cmd, choose one of $setList";
}

##############################################
# Get commands
##############################################
sub IntesisWMP_Get($@) {
    my ($hash, @a) = @_;
    my $name = shift @a;
    my $cmd = shift @a;
    my $value = join(" ", @a);
   
    return "\"get $name\" needs at least one argument" if(!defined($cmd));
   
    my $unit = AttrVal($name, "unit", $hash->{UNIT});
   
    # Build list of valid get commands
    my $getList = "update:noArg " .
                  "deviceInfo:noArg " .
                  "power:noArg " .
                  "mode:noArg " .
                  "desiredTemperature:noArg " .
                  "temperature:noArg " .
                  "fanSpeed:noArg " .
                  "vaneVertical:noArg " .
                  "vaneHorizontal:noArg " .
                  "limits:noArg " .
                  "raw";
   
    if($cmd eq "update") {
        Log3 $name, 3, "IntesisWMP ($name): Manual update triggered via get";
        IntesisWMP_GetAll($hash);
        return undef;
    }
    elsif($cmd eq "deviceInfo") {
        IntesisWMP_Send($hash, "ID");
        return undef;
    }
    elsif($cmd eq "power") {
        IntesisWMP_Send($hash, "GET,$unit:ONOFF");
        return undef;
    }
    elsif($cmd eq "mode") {
        IntesisWMP_Send($hash, "GET,$unit:MODE");
        return undef;
    }
    elsif($cmd eq "desiredTemperature" || $cmd eq "setptemp") {
        IntesisWMP_Send($hash, "GET,$unit:SETPTEMP");
        return undef;
    }
    elsif($cmd eq "temperature" || $cmd eq "ambtemp") {
        IntesisWMP_Send($hash, "GET,$unit:AMBTEMP");
        return undef;
    }
    elsif($cmd eq "fanSpeed" || $cmd eq "fanspeed") {
        IntesisWMP_Send($hash, "GET,$unit:FANSP");
        return undef;
    }
    elsif($cmd eq "vaneVertical" || $cmd eq "vaneud") {
        IntesisWMP_Send($hash, "GET,$unit:VANEUD");
        return undef;
    }
    elsif($cmd eq "vaneHorizontal" || $cmd eq "vanelr") {
        IntesisWMP_Send($hash, "GET,$unit:VANELR");
        return undef;
    }
    elsif($cmd eq "limits") {
        my @funcs = qw(ONOFF MODE FANSP VANEUD VANELR SETPTEMP);
        foreach my $func (@funcs) {
            IntesisWMP_Send($hash, "LIMITS:$func");
        }
        return undef;
    }
    elsif($cmd eq "raw") {
        IntesisWMP_Send($hash, $value);
        return undef;
    }
   
    return "Unknown command $cmd, choose one of $getList";
}

##############################################
# Attribute handling
##############################################
sub IntesisWMP_Attr(@) {
    my ($cmd, $name, $attrName, $attrVal) = @_;
    my $hash = $defs{$name};
   
    if($attrName eq "disable") {
        if($cmd eq "set" && $attrVal) {
            IntesisWMP_Disconnect($hash);
            $hash->{STATE} = "disabled";
        }
        elsif($cmd eq "del" || ($cmd eq "set" && !$attrVal)) {
            $hash->{STATE} = "Initialized";
            IntesisWMP_Connect($hash) if($init_done);
        }
    }
    elsif($attrName eq "interval") {
        if($cmd eq "set") {
            $attrVal = 10 if($attrVal < 10);
            $attrVal = 55 if($attrVal > 55);  # Must be < 60s
            $hash->{INTERVAL} = $attrVal;
            # Restart timer with new interval
            IntesisWMP_StartKeepaliveTimer($hash) if($hash->{CONNECTED});
        }
        else {
            $hash->{INTERVAL} = $WMP_KEEPALIVE_INTERVAL;
        }
    }
    elsif($attrName eq "updateInterval") {
        if($cmd eq "set") {
            $attrVal = 10 if($attrVal < 10);
            $hash->{UPDATE_INTERVAL} = $attrVal;
            Log3 $name, 3, "IntesisWMP ($name): Update interval changed to ${attrVal}s";
           
            # Restart update timer with new interval
            IntesisWMP_StartUpdateTimer($hash) if($hash->{CONNECTED});
        }
        else {
            $hash->{UPDATE_INTERVAL} = $WMP_UPDATE_INTERVAL;
        }
    }
    elsif($attrName eq "timeout") {
        if($cmd eq "set") {
            $hash->{TIMEOUT} = $attrVal;
        }
        else {
            $hash->{TIMEOUT} = $WMP_TIMEOUT;
        }
    }
    elsif($attrName eq "unit") {
        if($cmd eq "set") {
            $hash->{UNIT} = $attrVal;
        }
        else {
            $hash->{UNIT} = 1;
        }
    }
    elsif($attrName eq "pollFunctions") {
        if($cmd eq "set" && $attrVal) {
            # Validate functions
            my @funcs = split(/[,\s]+/, uc($attrVal));
            my @validFuncs = qw(ONOFF MODE SETPTEMP AMBTEMP FANSP VANEUD VANELR ERRSTATUS ERRCODE);
            my %validHash = map { $_ => 1 } @validFuncs;
            foreach my $func (@funcs) {
                if(!$validHash{$func}) {
                    return "Invalid function: $func. Valid functions: " . join(", ", @validFuncs);
                }
            }
        }
    }
   
    return undef;
}

##############################################
# Helper: trim whitespace
##############################################
sub IntesisWMP_trim($) {
    my ($str) = @_;
    return "" if(!defined($str));
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;
    return $str;
}

1;

=pod
=item device
=item summary    Control Intesis WMP AC devices (PA-AC-WMP-1)
=item summary_DE Steuerung von Intesis WMP Klimaanlagen (PA-AC-WMP-1)

=begin html

<a name="IntesisWMP"></a>
<h3>IntesisWMP</h3>
<ul>
  This module provides control for air conditioning devices via the Intesis WMP
  (WiFi Module for Panasonic AC) using the WMP ASCII protocol over TCP.<br><br>
 
  Supported devices include: PA-AC-WMP-1 (INWMPPAN001I000)<br><br>
 
  The module connects to the device on TCP port 3310 and maintains the connection
  with periodic keepalive messages. It handles spontaneous state change notifications
  from the device (CHN messages) and periodically polls for updates.<br><br>
 
  <a name="IntesisWMP_define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; IntesisWMP &lt;host&gt; [&lt;port&gt;]</code><br><br>
   
    <li><code>host</code>: IP address or hostname of the Intesis device</li>
    <li><code>port</code>: TCP port (default: 3310)</li><br>
   
    Example:<br>
    <code>define myAC IntesisWMP 192.168.1.100</code><br>
    <code>define myAC IntesisWMP ac.local 3310</code>
  </ul><br>

  <a name="IntesisWMP_set"></a>
  <b>Set</b>
  <ul>
    <li><b>on</b> - Turn AC on</li>
    <li><b>off</b> - Turn AC off</li>
    <li><b>mode</b> auto|heat|dry|fan|cool - Set operating mode</li>
    <li><b>desiredTemperature</b> &lt;temp&gt; - Set target temperature (16-32C)</li>
    <li><b>fanSpeed</b> auto|1-9 - Set fan speed</li>
    <li><b>vaneVertical</b> auto|1-9|swing - Set vertical vane position</li>
    <li><b>vaneHorizontal</b> auto|1-9|swing - Set horizontal vane position</li>
    <li><b>statusRequest</b> - Manually trigger an update of all readings</li>
    <li><b>reconnect</b> - Reconnect to the device</li>
    <li><b>raw</b> &lt;command&gt; - Send raw WMP command</li>
  </ul><br>
 
  <a name="IntesisWMP_get"></a>
  <b>Get</b>
  <ul>
    <li><b>update</b> - Refresh all readings from device</li>
    <li><b>deviceInfo</b> - Get device identification (ID command)</li>
    <li><b>power</b> - Get power state</li>
    <li><b>mode</b> - Get current mode</li>
    <li><b>desiredTemperature</b> - Get target temperature</li>
    <li><b>temperature</b> - Get ambient temperature</li>
    <li><b>fanSpeed</b> - Get fan speed</li>
    <li><b>vaneVertical</b> - Get vertical vane position</li>
    <li><b>vaneHorizontal</b> - Get horizontal vane position</li>
    <li><b>limits</b> - Get all function limits</li>
    <li><b>raw</b> &lt;command&gt; - Send raw WMP command</li>
  </ul><br>
 
  <a name="IntesisWMP_readings"></a>
  <b>Readings</b>
  <ul>
    <li><b>state</b> - Connection state / current mode when on</li>
    <li><b>power</b> - Power state (on/off)</li>
    <li><b>mode</b> - Operating mode (auto/heat/dry/fan/cool)</li>
    <li><b>desiredTemperature</b> - Target temperature in C</li>
    <li><b>temperature</b> - Ambient/room temperature in C</li>
    <li><b>fanSpeed</b> - Fan speed (auto/1-9)</li>
    <li><b>vaneVertical</b> - Vertical vane position</li>
    <li><b>vaneHorizontal</b> - Horizontal vane position</li>
    <li><b>model</b> - Device model</li>
    <li><b>mac</b> - MAC address</li>
    <li><b>ip</b> - IP address</li>
    <li><b>firmware</b> - Firmware version</li>
    <li><b>rssi</b> - WiFi signal strength</li>
  </ul><br>
 
  <a name="IntesisWMP_attr"></a>
  <b>Attributes</b>
  <ul>
    <li><b>disable</b> 0|1 - Disable the device</li>
    <li><b>interval</b> 10-55 - Keepalive interval in seconds (default: 30)</li>
    <li><b>updateInterval</b> - Interval for periodic polling of all values in seconds (default: 60, min: 10)</li>
    <li><b>timeout</b> - Connection timeout in seconds (default: 5)</li>
    <li><b>unit</b> - Unit number for multi-unit installations (default: 1)</li>
    <li><b>username</b> - Username for secured devices</li>
    <li><b>password</b> - Password for secured devices</li>
    <li><b>tempUnit</b> C|F - Temperature unit for display</li>
    <li><b>pollFunctions</b> - Comma-separated list of functions to poll (default: ONOFF,MODE,SETPTEMP,AMBTEMP,FANSP,VANEUD,VANELR)</li>
  </ul><br>
 
  <b>RSSI Signal Quality</b><br>
  <ul>
    <li>&gt; -70: Excellent</li>
    <li>-71 to -81: Very Good</li>
    <li>-81 to -85: Good</li>
    <li>-86 to -95: Weak</li>
    <li>&lt; -96: Bad</li>
  </ul>
</ul>

=end html

=begin html_DE

<a name="IntesisWMP"></a>
<h3>IntesisWMP</h3>
<ul>
  Dieses Modul ermoeglicht die Steuerung von Klimageraeten ueber das Intesis WMP
  (WiFi-Modul fuer Panasonic Klimaanlagen) mittels WMP ASCII-Protokoll ueber TCP.<br><br>
 
  Unterstuetzte Geraete: PA-AC-WMP-1 (INWMPPAN001I000)<br><br>
 
  Das Modul verbindet sich ueber TCP-Port 3310 mit dem Geraet und haelt die Verbindung
  durch periodische Keepalive-Nachrichten aufrecht. Spontane Statusaenderungen vom
  Geraet (CHN-Nachrichten) werden automatisch verarbeitet. Zusaetzlich werden alle
  Werte periodisch abgefragt.<br><br>
 
  <a name="IntesisWMP_define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; IntesisWMP &lt;host&gt; [&lt;port&gt;]</code><br><br>
   
    Beispiel:<br>
    <code>define meineKlima IntesisWMP 192.168.1.100</code>
  </ul><br>
 
  <b>Attribute</b>
  <ul>
    <li><b>updateInterval</b> - Intervall fuer periodische Abfrage aller Werte in Sekunden (Standard: 60, Min: 10)</li>
    <li><b>pollFunctions</b> - Komma-getrennte Liste der abzufragenden Funktionen</li>
  </ul>
</ul>

=end html_DE

=cut