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
Automatic status monitoring every 60 seconds
Intelligent error handling with 3-error threshold for connection issues
Start/Stop/Dock commands
Cutting height control
Battery monitoring with low battery warnings
GPS position tracking
Schedule management
Connection status for system monitoring
Sleep time integration (postpones notifications during night hours)
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
- Adjust IP Address: Change
DEVICE_IP
inSUNSEEKER_CONFIG
- Set Email Addresses: Replace
'your-email@example.com'
with your notification email - Customize Items: Adjust item names to match your setup
- 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!
Note: Remember to test thoroughly and adjust the configuration to match your specific mower model and network setup.
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.
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:
Our cats watching the lawn
Our new sprinkler system (and why it triggered a redesign)
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.
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.