Sunseeker Lawn-Mower: openHAB Proof-of-Concept (JavaScript Rules) (testers welcome!)

This integration provides complete control and monitoring of Sunseeker lawn mowers through openHAB using JavaScript rules. Based on the Sunseeker Lawn Mower Python library, this implementation offers robust error handling, intelligent notifications, and seasonal activation/deactivation.

Features

:white_check_mark: Automatic status monitoring every 60 seconds
:white_check_mark: Intelligent error handling with 3-error threshold for connection issues
:white_check_mark: Start/Stop/Dock commands
:white_check_mark: Cutting height control
:white_check_mark: Battery monitoring with low battery warnings
:white_check_mark: GPS position tracking
:white_check_mark: Schedule management
:white_check_mark: Connection status for system monitoring
:white_check_mark: Sleep time integration (postpones notifications during night hours)
:white_check_mark: Seasonal activation (summer/winter mode)

// Control Items
Switch Sunseeker_Active "Sunseeker Monitoring Active" <switch>
Switch Sunseeker_Connection_Status "Sunseeker Connection" {autoupdate="false"}

// Status Items  
String Sunseeker_State "Mower Status" 
Number Sunseeker_Battery "Battery [%d %%]" <battery>
Number Sunseeker_Signal "Signal Strength [%d %%]"
Number Sunseeker_Error_Code "Error Code" <error>
Number Sunseeker_Cutting_Height "Cutting Height [%d mm]" <settings>
Number Sunseeker_Total_Time "Total Runtime [%d h]" <time>
Number Sunseeker_Working_Time "Working Time [%d h]" <time>
String Sunseeker_Position "GPS Position" 
String Sunseeker_Last_Update "Last Update" <time>

// Command Items
Switch Sunseeker_Start_Stop "Start/Stop" <switch>
Switch Sunseeker_Go_Home "Go to Dock" <house>
Number Sunseeker_Set_Cutting_Height "Set Cutting Height [%d mm]" <settings>
Switch Sunseeker_Schedule_Enable "Schedule Active" <calendar>

// Additional Items (adjust to your setup)
Switch Advanced_Logging "Advanced Logging" <switch>
Switch Sleep_Time "Sleep Time"

JavaScript Integration

Save as sunseeker.js in your js folder:

const { rules, items, time, actions, triggers } = require('openhab');

// Sunseeker Configuration - ADJUST TO YOUR SETUP
const SUNSEEKER_CONFIG = {
    DEVICE_IP: '192.168.1.100', // IP address of your mower
    HTTP_PORT: 80,
    POLL_INTERVAL: 60000, // 1 minute
    COMMAND_TIMEOUT: 10000, // 10 seconds
    MAX_RETRIES: 3
};

// Global Variables
let connectionFailureCount = 0;
let activeErrorTypes = new Map();
let lastStatusUpdate = null;

// Logger function
const log = (message, level = 'info') => {
    try {
        const advancedLogging = items.getItem('Advanced_Logging').state === 'ON';
        if (advancedLogging || level === 'error') {
            const prefix = 'SUNSEEKER';
            switch (level) {
                case 'debug': console.debug(`[${prefix}] ${message}`); break;
                case 'warn': console.warn(`[${prefix}] ${message}`); break;
                case 'error': console.error(`[${prefix}] ${message}`); break;
                default: console.info(`[${prefix}] ${message}`);
            }
        }
    } catch (e) {
        console.info(`[SUNSEEKER] ${message}`);
    }
};

// Error messaging with intelligent threshold
function sendConnectionErrorMessage(message, errorType = 'general') {
    if (items.getItem('Sunseeker_Active').state === 'OFF') {
        log('Sunseeker monitoring disabled.', 'debug');
        return;
    }

    const now = new Date();
    
    // 3-error threshold for connection problems
    if (errorType === 'connection') {
        connectionFailureCount++;
        if (connectionFailureCount < 3) {
            log(`Connection error ${connectionFailureCount}/3 - no notification yet`, 'warn');
            return;
        }
        log(`Connection error ${connectionFailureCount}/3 - sending notification`, 'error');
    }
    
    // Check if this error type is already active
    if (activeErrorTypes.has(errorType) && 
        (now.getTime() - activeErrorTypes.get(errorType).getTime()) < 60 * 60 * 1000) {
        log(`Error type ${errorType} already reported, waiting.`, 'debug');
        return;
    }
    
    activeErrorTypes.set(errorType, now);
    
    // Set connection status to OFF
    try {
        items.getItem('Sunseeker_Connection_Status').postUpdate('OFF');
    } catch (e) {
        log(`Error setting Sunseeker_Connection_Status: ${e}`, 'warn');
    }
    
    log(message, 'error');

    // Check sleep time - ADJUST TO YOUR SLEEP TIME ITEM
    try {
        if (items.getItem('Sleep_Time').state === 'ON') {
            log(`Notification postponed (sleep time active)`, 'info');
            return;
        }
    } catch (e) {
        // Sleep time item not available, continue
    }

    // Send notification - ADJUST EMAIL ADDRESSES
    ['your-email@example.com'].forEach(userId => {
        try {
            actions.notificationBuilder(message)
                .withTitle("Sunseeker Mower Problem")
                .withTag("Sunseeker_Error")
                .withIcon("error")
                .withReferenceId(`sunseeker-error-${errorType}-${Date.now()}`)
                .addUserId(userId)
                .send();
        } catch (e) {
            log(`Failed to send notification: ${e}`, 'warn');
        }
    });
}

// Success error handling
function sendErrorResolvedMessage(errorType) {
    if (!activeErrorTypes.has(errorType)) {
        return;
    }
    
    const errorTime = activeErrorTypes.get(errorType);
    const now = new Date();
    const durationMinutes = Math.round((now.getTime() - errorTime.getTime()) / (60 * 1000));
    
    const message = `✅ Sunseeker ${errorType} problem resolved (lasted ${durationMinutes} minutes)`;
    log(message, 'info');
    
    activeErrorTypes.delete(errorType);
    
    if (errorType === 'connection') {
        connectionFailureCount = 0;
    }
    
    // Set connection status to ON if no errors remain
    if (activeErrorTypes.size === 0) {
        try {
            items.getItem('Sunseeker_Connection_Status').postUpdate('ON');
        } catch (e) {
            log(`Error resetting Sunseeker_Connection_Status: ${e}`, 'warn');
        }
    }
    
    // Send notification - ADJUST EMAIL ADDRESSES
    try {
        if (items.getItem('Sleep_Time').state === 'OFF') {
            ['your-email@example.com'].forEach(userId => {
                try {
                    actions.notificationBuilder(message)
                        .withTitle("Sunseeker Problem Resolved")
                        .withTag("Sunseeker_Resolved")
                        .withIcon("system_update")
                        .withReferenceId(`sunseeker-resolved-${errorType}-${Date.now()}`)
                        .addUserId(userId)
                        .send();
                } catch (e) {
                    log(`Failed to send resolved notification: ${e}`, 'warn');
                }
            });
        }
    } catch (e) {
        // Sleep time item not available, continue
    }
}

// HTTP Request Helper
async function sendHttpRequest(path, method = 'GET', body = null) {
    try {
        const url = `http://${SUNSEEKER_CONFIG.DEVICE_IP}:${SUNSEEKER_CONFIG.HTTP_PORT}${path}`;
        log(`Sending ${method} request: ${url}`, 'debug');
        
        let response;
        if (method === 'GET') {
            response = await actions.HTTP.sendHttpGetRequest(url, {}, SUNSEEKER_CONFIG.COMMAND_TIMEOUT);
        } else {
            response = await actions.HTTP.sendHttpPostRequest(
                url, 
                'application/json', 
                body || '', 
                {}, 
                SUNSEEKER_CONFIG.COMMAND_TIMEOUT
            );
        }
        
        if (!response) {
            throw new Error('No response from mower');
        }
        
        log(`Response: ${response}`, 'debug');
        return JSON.parse(response);
        
    } catch (error) {
        log(`HTTP Request Error: ${error}`, 'error');
        throw error;
    }
}

// Get mower status
async function getMowerStatus() {
    try {
        const statusData = await sendHttpRequest('/api/v1/status');
        
        // Successful connection - reset errors
        if (connectionFailureCount >= 3 || activeErrorTypes.has('connection')) {
            sendErrorResolvedMessage('connection');
        }
        connectionFailureCount = 0;
        lastStatusUpdate = new Date();
        
        // Update status items
        updateStatusItems(statusData);
        
        log('Status successfully updated', 'info');
        return true;
        
    } catch (error) {
        sendConnectionErrorMessage(`Sunseeker status query failed: ${error}`, 'connection');
        return false;
    }
}

// Update status items
function updateStatusItems(data) {
    try {
        // Basic status updates
        if (data.state !== undefined) {
            items.getItem('Sunseeker_State').postUpdate(mapMowerState(data.state));
        }
        
        if (data.battery_level !== undefined) {
            items.getItem('Sunseeker_Battery').postUpdate(data.battery_level);
        }
        
        if (data.signal_strength !== undefined) {
            items.getItem('Sunseeker_Signal').postUpdate(data.signal_strength);
        }
        
        if (data.error_code !== undefined) {
            items.getItem('Sunseeker_Error_Code').postUpdate(data.error_code);
            
            // Error handling
            if (data.error_code !== 0) {
                const errorMessage = getErrorMessage(data.error_code);
                sendConnectionErrorMessage(`Sunseeker Error: ${errorMessage} (Code: ${data.error_code})`, 'device_error');
            } else if (activeErrorTypes.has('device_error')) {
                sendErrorResolvedMessage('device_error');
            }
        }
        
        if (data.cutting_height !== undefined) {
            items.getItem('Sunseeker_Cutting_Height').postUpdate(data.cutting_height);
        }
        
        if (data.total_time !== undefined) {
            items.getItem('Sunseeker_Total_Time').postUpdate(Math.round(data.total_time / 60)); // Minutes to hours
        }
        
        if (data.working_time !== undefined) {
            items.getItem('Sunseeker_Working_Time').postUpdate(Math.round(data.working_time / 60));
        }
        
        // GPS position if available
        if (data.latitude !== undefined && data.longitude !== undefined) {
            items.getItem('Sunseeker_Position').postUpdate(`${data.latitude},${data.longitude}`);
        }
        
        // Last update
        items.getItem('Sunseeker_Last_Update').postUpdate(new Date().toLocaleString());
        
    } catch (error) {
        log(`Error updating status items: ${error}`, 'error');
    }
}

// Map mower state
function mapMowerState(state) {
    const stateMap = {
        0: 'Idle',
        1: 'Mowing',
        2: 'Charging',
        3: 'Error',
        4: 'Going Home',
        5: 'Paused',
        6: 'Rain Delay',
        7: 'Manual Mode'
    };
    return stateMap[state] || `Unknown (${state})`;
}

// Translate error codes
function getErrorMessage(errorCode) {
    const errorMap = {
        0: 'No Error',
        1: 'Blade Blocked',
        2: 'Battery Low',
        3: 'Outside Boundary',
        4: 'Rain Detected',
        5: 'Mower Tilted',
        6: 'Mower Lifted',
        7: 'Charging Station Not Found',
        8: 'Motor Overheated',
        9: 'Obstacle Detected',
        10: 'GPS Signal Lost'
    };
    return errorMap[errorCode] || `Unknown Error (${errorCode})`;
}

// Send commands to mower
async function sendMowerCommand(command, parameters = {}) {
    try {
        log(`Sending command: ${command}`, 'info');
        
        const commandData = {
            command: command,
            ...parameters
        };
        
        const response = await sendHttpRequest('/api/v1/command', 'POST', JSON.stringify(commandData));
        
        if (response.success) {
            log(`Command ${command} sent successfully`, 'info');
            
            // Update status after command (with delay)
            setTimeout(async () => {
                await getMowerStatus();
            }, 2000);
            
            return true;
        } else {
            throw new Error(response.message || 'Command failed');
        }
        
    } catch (error) {
        const errorMessage = `Sunseeker command ${command} failed: ${error}`;
        log(errorMessage, 'error');
        sendConnectionErrorMessage(errorMessage, 'command_error');
        return false;
    }
}

// RULES

// Regular status updates
rules.JSRule({
    name: "Sunseeker_Status_Update",
    description: "Regularly queries Sunseeker mower status",
    triggers: [triggers.GenericCronTrigger("0 */1 * * * ?")], // Every 1 minute
    execute: async () => {
        if (items.getItem('Sunseeker_Active').state === 'ON') {
            await getMowerStatus();
        } else {
            log('Sunseeker monitoring disabled', 'debug');
        }
    }
});

// Start/Stop control
rules.JSRule({
    name: "Sunseeker_Start_Stop_Control",
    description: "Starts or stops the mower",
    triggers: [triggers.ItemCommandTrigger("Sunseeker_Start_Stop")],
    execute: async (event) => {
        if (event.receivedCommand === 'ON') {
            await sendMowerCommand('start');
        } else if (event.receivedCommand === 'OFF') {
            await sendMowerCommand('stop');
        }
    }
});

// Home command
rules.JSRule({
    name: "Sunseeker_Go_Home",
    description: "Sends mower to charging station",
    triggers: [triggers.ItemCommandTrigger("Sunseeker_Go_Home")],
    execute: async (event) => {
        if (event.receivedCommand === 'ON') {
            await sendMowerCommand('dock');
        }
    }
});

// Cutting height control
rules.JSRule({
    name: "Sunseeker_Cutting_Height_Control",
    description: "Changes mower cutting height",
    triggers: [triggers.ItemCommandTrigger("Sunseeker_Set_Cutting_Height")],
    execute: async (event) => {
        const height = parseInt(event.receivedCommand);
        if (!isNaN(height) && height >= 20 && height <= 80) {
            await sendMowerCommand('set_cutting_height', { height: height });
        } else {
            log(`Invalid cutting height: ${event.receivedCommand} (20-80mm allowed)`, 'warn');
        }
    }
});

// Schedule control
rules.JSRule({
    name: "Sunseeker_Schedule_Control", 
    description: "Enables/disables automatic schedule",
    triggers: [triggers.ItemCommandTrigger("Sunseeker_Schedule_Enable")],
    execute: async (event) => {
        const enable = event.receivedCommand === 'ON';
        await sendMowerCommand('schedule', { enabled: enable });
    }
});

// Sunseeker_Active status handler
rules.JSRule({
    name: "Sunseeker_Connection_Status_Handler",
    description: "Manages Sunseeker_Connection_Status based on Sunseeker_Active",
    triggers: [
        triggers.ItemStateChangeTrigger('Sunseeker_Active', undefined, 'ON'),
        triggers.ItemStateChangeTrigger('Sunseeker_Active', undefined, 'OFF')
    ],
    execute: async (event) => {
        if (event.receivedState.toString() === 'OFF') {
            log('Sunseeker disabled - reset connection status', 'info');
            
            // Reset all errors
            activeErrorTypes.clear();
            connectionFailureCount = 0;
            
            // Connection_Status to ON (offline expected)
            try {
                items.getItem('Sunseeker_Connection_Status').postUpdate('ON');
            } catch (e) {
                log(`Error resetting Sunseeker_Connection_Status: ${e}`, 'warn');
            }
            
        } else if (event.receivedState.toString() === 'ON') {
            log('Sunseeker enabled - initialize connection status', 'info');
            
            // Initially set to ON
            try {
                items.getItem('Sunseeker_Connection_Status').postUpdate('ON');
            } catch (e) {
                log(`Error initializing Sunseeker_Connection_Status: ${e}`, 'warn');
            }
            
            // First status check after 10 seconds
            setTimeout(async () => {
                log('Performing initial status check after Sunseeker activation', 'info');
                await getMowerStatus();
            }, 10000);
        }
    }
});

// Low battery warning
rules.JSRule({
    name: "Sunseeker_Low_Battery_Warning",
    description: "Warns about low battery level",
    triggers: [triggers.ItemStateChangeTrigger("Sunseeker_Battery")],
    execute: (event) => {
        const batteryLevel = parseInt(event.receivedState);
        
        if (!isNaN(batteryLevel) && batteryLevel <= 15) {
            const message = `⚠️ Sunseeker mower has low battery: ${batteryLevel}%`;
            
            try {
                if (items.getItem('Sleep_Time').state === 'OFF') {
                    ['your-email@example.com'].forEach(userId => {
                        try {
                            actions.notificationBuilder(message)
                                .withTitle("Mower Battery Low")
                                .withTag("Sunseeker_Low_Battery")
                                .withIcon("battery")
                                .withReferenceId(`sunseeker-battery-${Date.now()}`)
                                .addUserId(userId)
                                .send();
                        } catch (e) {
                            log(`Failed to send battery notification: ${e}`, 'warn');
                        }
                    });
                }
            } catch (e) {
                // Sleep time item not available, continue
            }
            
            log(message, 'warn');
        }
    }
});

// Module load notification
rules.JSRule({
    name: "Sunseeker Module Load",
    description: "Shows Sunseeker module loading",
    triggers: [
        triggers.ItemStateChangeTrigger('openhab_Online', undefined, 'ON') // Adjust to your system online item
    ],
    execute: () => {
        log("sunseeker.js loaded", 'info');
    }
});

Configuration

  1. Adjust IP Address: Change DEVICE_IP in SUNSEEKER_CONFIG
  2. Set Email Addresses: Replace 'your-email@example.com' with your notification email
  3. Customize Items: Adjust item names to match your setup
  4. API Endpoints: Modify API paths based on your mower’s actual API structure

Features Explained

Intelligent Error Handling

  • Connection errors: Only notify after 3 consecutive failures
  • Device errors: Immediate notification for mower-specific problems
  • Auto-recovery: Automatically marks errors as resolved

Seasonal Activation

  • Summer: Sunseeker_Active = ON - Full monitoring and control
  • Winter: Sunseeker_Active = OFF - No monitoring, no error notifications

Battery Management

  • Low battery warnings at 15%
  • Runtime tracking (total and working hours)

Advanced Features

  • GPS position tracking
  • Signal strength monitoring
  • Cutting height adjustment (20-80mm range)
  • Schedule management
  • Sleep time integration

API Compatibility

This integration is designed to work with the Sunseeker mower API structure. You may need to adjust:

  • API endpoints (/api/v1/status, /api/v1/command)
  • Data field names based on your mower’s API
  • Error codes and state mappings

Dependencies

  • openHAB 4.x with JavaScript rules support
  • Network connectivity to mower
  • HTTP binding for API calls

Enjoy your automated lawn care! :seedling:

Note: Remember to test thoroughly and adjust the configuration to match your specific mower model and network setup.

:magnifying_glass_tilted_left: Looking for testers

At the moment, I don’t have a mower yet, so the implementation hasn’t been tested against real hardware.
If you’re already using a Sunseeker mower (especially the X3 or X7 series), I’d love your help testing and improving the integration.

:trophy: Bonus: Win a Sunseeker Mower?

There’s currently a Sunseeker mower giveaway running until August 15th in the official Facebook community —
just search for “Sunseeker Elite X Series Official Group”.

They’re looking for real-world testers and smart-home enthusiasts to share feedback on the X3 and X7 models.
I’ve already joined and submitted my entry, including posts about:

  • :paw_prints: Our cats watching the lawn
  • :herb: Our new sprinkler system (and why it triggered a redesign)
  • :mobile_phone: A custom smart home dashboard for mower integration

If you’re interested in integrating the mower into your smart home — and maybe even winning one — this is a great time to get involved.

:light_bulb: Note: I’m not affiliated with Sunseeker — just genuinely interested in supporting these devices in openHAB, exchanging ideas with other users, and maybe inspiring someone more experienced with openHAB bindings to take this further and build a native integration.

1 Like