conexocasa há 7 meses atrás
commit
1e3c13ab14

+ 176 - 0
CHANGELOG.md

@@ -0,0 +1,176 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [2.1.1] - 2026-03-05
+
+### Added
+- `.github/PULL_REQUEST_TEMPLATE.md` — standardised PR template for HACS default submission.
+
+### Changed
+- `hacs.json` updated: added all integration platforms (`sensor`, `number`, `select`, `switch`), enabled `zip_release: true`, and set `filename: solar-of-things-ha.zip` so HACS fetches the correct release asset.
+- `manifest.json` version bumped to 2.1.1.
+
+## [2.1.0] - 2026-02-26
+
+### Added
+- Auto-discover all device IDs under a station using `POST /apis/device/list` (no need to manually enter deviceId).
+- Config flow support for optional `device_id`:
+  - Leave blank to create entities for *all* devices found under the station.
+  - Provide a `device_id` to restrict HA entities to a single device.
+
+### Changed
+- Manifest metadata updated (documentation/issue tracker URLs) and integration version bumped to 2.1.0.
+
+## [2.0.0] - 2024-02-10
+
+### Added
+- **Control Features**: Full system control capabilities
+  - Number entities for battery charge/discharge limits (0-100%)
+  - Number entity for grid charge power limit (0-5000W)
+  - Select entity for operating mode (Self-Use, Time-of-Use, Backup, Grid-Tie, Off-Grid)
+  - Select entity for battery priority (Solar First, Battery First, Grid First)
+  - Switch entity for grid charging enable/disable
+  - Switch entity for grid feed-in enable/disable
+  - Switch entity for backup mode enable/disable
+- Settings API integration for reading and updating device configuration
+- Comprehensive control documentation (CONTROL_FEATURES.md)
+- Automation examples for time-of-use optimization
+- Weather-based battery management examples
+- Emergency backup preparation automations
+- Dashboard control panel examples
+
+### Changed
+- Updated coordinator to fetch device settings
+- Enhanced API client with control methods
+- Updated README with control features documentation
+- Extended translations for new entities
+
+### Technical Details
+- New platforms: Number, Select, Switch
+- API endpoints: `/api/device/settings/v1`, `/api/device/settings/update/v1`
+- Immediate coordinator refresh after control changes
+- Proper error handling for control operations
+
+## [1.0.0] - 2024-02-10
+
+### Added
+- Initial release of Solar of Things integration
+- Real-time sensor monitoring via API
+- Support for multiple stations/devices configuration
+- Config flow for easy setup via UI
+- Comprehensive sensor suite:
+  - PV Input Power
+  - AC Output Power
+  - Battery Discharge/Charge Current
+  - Battery Voltage
+  - Battery State of Charge
+  - Battery Power (calculated)
+  - Grid Feed-in Power
+  - Grid Import Power (calculated)
+  - Load Power
+  - Monthly PV Generation (when station ID provided)
+- Auto-calculated metrics for battery power and grid usage
+- 5-minute update interval with configurable options
+- Home Assistant Energy Dashboard compatibility
+- Device information for each configured station
+- Proper device classes and state classes for all sensors
+- Error handling and connection validation
+- Support for IOT-Token authentication
+- Multi-station support through multiple config entries
+- Comprehensive documentation and examples
+
+### Technical Details
+- API integration with Solar of Things (Siseli) platform
+- Uses time-series and monthly summary API endpoints
+- Automatic data fetching via coordinator pattern
+- Proper unit conversions (W, A, V, kWh, %)
+- Timezone handling (GMT+8)
+- Request throttling and error recovery
+
+## [Unreleased]
+
+### Planned Features
+- Historical data import for energy statistics
+- Configurable calculation methods for derived sensors
+- Additional monthly statistics sensors
+- Support for multiple devices per station
+- Advanced battery cycle tracking
+- Cost calculation sensors
+- Solar forecast integration
+- Customizable sensor polling rates per sensor
+- Support for additional inverter models
+- Webhook support for real-time updates
+- MQTT integration option
+- RESTful sensor alternatives
+- Diagnostic sensors for API health
+- Binary sensors for status alerts
+
+### Known Issues
+- Grid import calculation may not exactly match utility bills
+- Monthly API doesn't track grid import/export (always returns 0)
+- Timezone hardcoded to Asia/Manila (GMT+8)
+- Token expiration requires manual renewal
+- No retry logic for failed API calls (handled by coordinator)
+
+### Considerations for Future Versions
+- Add support for custom timezone configuration
+- Implement automatic token refresh mechanism
+- Add support for historical data backfill
+- Create services for on-demand data refresh
+- Add diagnostic mode for troubleshooting
+- Support for multiple simultaneous time ranges
+- Caching layer for API responses
+- Rate limiting protection
+- Support for alternative authentication methods
+
+---
+
+## Version History
+
+### How to Upgrade
+
+To upgrade to a new version:
+
+1. **Via HACS**:
+   - Go to HACS → Integrations
+   - Find "Solar of Things"
+   - Click "Update"
+   - Restart Home Assistant
+
+2. **Manual**:
+   - Download the new version
+   - Replace files in `/config/custom_components/solar_of_things/`
+   - Restart Home Assistant
+
+### Breaking Changes
+
+None yet (version 1.0.0)
+
+### Migration Guide
+
+None required for version 1.0.0
+
+---
+
+## Release Notes Format
+
+Each release includes:
+- **Added**: New features
+- **Changed**: Changes in existing functionality
+- **Deprecated**: Soon-to-be removed features
+- **Removed**: Removed features
+- **Fixed**: Bug fixes
+- **Security**: Security improvements
+
+---
+
+## Support
+
+For questions about changes or upgrade issues:
+- Check the [README](README.md)
+- Review [INSTALLATION.md](INSTALLATION.md)
+- Open an issue on GitHub

+ 617 - 0
CONTROL_FEATURES.md

@@ -0,0 +1,617 @@
+# Solar System Control Features
+
+## Overview
+
+The Solar of Things integration now supports **full control** of your solar system settings through Home Assistant. You can adjust battery limits, change operating modes, and control grid interactions directly from your dashboard or automations.
+
+---
+
+## 🎛️ Control Entities
+
+### Number Entities (Sliders)
+
+#### 1. **Battery Charge Limit**
+- **Type**: Number (0-100%)
+- **Description**: Maximum battery charge level
+- **Use Case**: Preserve battery health by limiting charge to 90%
+- **Entity**: `number.solar_station_xxxxx_battery_charge_limit`
+
+```yaml
+# Set battery charge limit to 90%
+service: number.set_value
+target:
+  entity_id: number.solar_station_xxxxx_battery_charge_limit
+data:
+  value: 90
+```
+
+#### 2. **Battery Discharge Limit**
+- **Type**: Number (0-100%)
+- **Description**: Minimum battery level before stopping discharge
+- **Use Case**: Keep battery reserve for emergencies
+- **Entity**: `number.solar_station_xxxxx_battery_discharge_limit`
+
+```yaml
+# Don't discharge below 20%
+service: number.set_value
+target:
+  entity_id: number.solar_station_xxxxx_battery_discharge_limit
+data:
+  value: 20
+```
+
+#### 3. **Grid Charge Limit**
+- **Type**: Number (0-5000W)
+- **Description**: Maximum power to draw from grid for battery charging
+- **Use Case**: Charge from grid during off-peak hours
+- **Entity**: `number.solar_station_xxxxx_grid_charge_limit`
+
+```yaml
+# Allow 2000W grid charging
+service: number.set_value
+target:
+  entity_id: number.solar_station_xxxxx_grid_charge_limit
+data:
+  value: 2000
+```
+
+---
+
+### Select Entities (Dropdowns)
+
+#### 1. **Operating Mode**
+- **Type**: Select
+- **Options**:
+  - **Self-Use**: Maximize self-consumption
+  - **Time-of-Use**: Optimize for electricity rates
+  - **Backup**: Prioritize battery reserve
+  - **Grid-Tie**: Prioritize grid export
+  - **Off-Grid**: Independent operation
+- **Entity**: `select.solar_station_xxxxx_operating_mode`
+
+```yaml
+# Switch to Time-of-Use mode
+service: select.select_option
+target:
+  entity_id: select.solar_station_xxxxx_operating_mode
+data:
+  option: "Time-of-Use"
+```
+
+#### 2. **Battery Priority**
+- **Type**: Select
+- **Options**:
+  - **Solar First**: Use solar before battery
+  - **Battery First**: Use battery before solar
+  - **Grid First**: Use grid before battery/solar
+- **Entity**: `select.solar_station_xxxxx_battery_priority`
+
+```yaml
+# Prioritize solar energy
+service: select.select_option
+target:
+  entity_id: select.solar_station_xxxxx_battery_priority
+data:
+  option: "Solar First"
+```
+
+---
+
+### Switch Entities (On/Off)
+
+#### 1. **Grid Charging**
+- **Type**: Switch
+- **Description**: Allow battery charging from grid
+- **Use Case**: Enable during cheap electricity periods
+- **Entity**: `switch.solar_station_xxxxx_grid_charging`
+
+```yaml
+# Enable grid charging
+service: switch.turn_on
+target:
+  entity_id: switch.solar_station_xxxxx_grid_charging
+```
+
+#### 2. **Grid Feed-In**
+- **Type**: Switch
+- **Description**: Allow exporting excess power to grid
+- **Use Case**: Disable when grid feed-in is not compensated
+- **Entity**: `switch.solar_station_xxxxx_grid_feed_in`
+
+```yaml
+# Disable grid export
+service: switch.turn_off
+target:
+  entity_id: switch.solar_station_xxxxx_grid_feed_in
+```
+
+#### 3. **Backup Mode**
+- **Type**: Switch
+- **Description**: Reserve battery for emergencies
+- **Use Case**: Prepare for expected power outage
+- **Entity**: `switch.solar_station_xxxxx_backup_mode`
+
+```yaml
+# Enable backup reserve
+service: switch.turn_on
+target:
+  entity_id: switch.solar_station_xxxxx_backup_mode
+```
+
+---
+
+## 🤖 Automation Examples
+
+### 1. Time-of-Use Optimization
+
+Charge from grid during cheap hours, discharge during peak hours:
+
+```yaml
+automation:
+  # Charge from grid during cheap period (midnight-6am)
+  - alias: "Solar: Charge from Grid (Off-Peak)"
+    trigger:
+      - platform: time
+        at: "00:00:00"
+    action:
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_grid_charge_limit
+        data:
+          value: 3000  # 3kW charging
+
+  # Stop grid charging after cheap period
+  - alias: "Solar: Stop Grid Charging (Peak)"
+    trigger:
+      - platform: time
+        at: "06:00:00"
+    action:
+      - service: switch.turn_off
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_grid_charge_limit
+        data:
+          value: 0
+```
+
+### 2. Weather-Based Battery Management
+
+Adjust battery limits based on weather forecast:
+
+```yaml
+automation:
+  - alias: "Solar: Adjust for Cloudy Weather"
+    trigger:
+      - platform: state
+        entity_id: weather.home
+        attribute: condition
+        to: "cloudy"
+    action:
+      # Lower discharge limit on cloudy days
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_discharge_limit
+        data:
+          value: 40  # Keep 40% reserve
+      
+      # Enable grid charging if needed
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+
+  - alias: "Solar: Adjust for Sunny Weather"
+    trigger:
+      - platform: state
+        entity_id: weather.home
+        attribute: condition
+        to: "sunny"
+    action:
+      # Normal discharge limit on sunny days
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_discharge_limit
+        data:
+          value: 10
+      
+      # Disable grid charging (solar is sufficient)
+      - service: switch.turn_off
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+```
+
+### 3. Emergency Backup Preparation
+
+Prepare for storms or power outages:
+
+```yaml
+automation:
+  - alias: "Solar: Storm Preparation"
+    trigger:
+      - platform: state
+        entity_id: sensor.weather_alert
+        to: "storm_warning"
+    action:
+      # Enable backup mode
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_backup_mode
+      
+      # Set high charge limit
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_charge_limit
+        data:
+          value: 100
+      
+      # Set high discharge limit (preserve battery)
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_discharge_limit
+        data:
+          value: 50
+      
+      # Enable grid charging to top up
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+      
+      # Notify user
+      - service: notify.mobile_app
+        data:
+          title: "Solar System: Storm Preparation"
+          message: "Battery is being prepared for potential outage"
+```
+
+### 4. Dynamic Operating Mode
+
+Switch modes based on electricity prices:
+
+```yaml
+automation:
+  - alias: "Solar: Peak Price Mode"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.electricity_price
+        above: 0.25  # High price threshold
+    action:
+      - service: select.select_option
+        target:
+          entity_id: select.solar_station_xxxxx_operating_mode
+        data:
+          option: "Time-of-Use"
+      
+      # Maximize battery usage during peak
+      - service: select.select_option
+        target:
+          entity_id: select.solar_station_xxxxx_battery_priority
+        data:
+          option: "Battery First"
+
+  - alias: "Solar: Off-Peak Mode"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.electricity_price
+        below: 0.10  # Low price threshold
+    action:
+      - service: select.select_option
+        target:
+          entity_id: select.solar_station_xxxxx_operating_mode
+        data:
+          option: "Self-Use"
+      
+      # Prioritize solar during off-peak
+      - service: select.select_option
+        target:
+          entity_id: select.solar_station_xxxxx_battery_priority
+        data:
+          option: "Solar First"
+```
+
+### 5. Battery Health Protection
+
+Extend battery life with charge cycling:
+
+```yaml
+automation:
+  - alias: "Solar: Weekly Battery Calibration"
+    trigger:
+      - platform: time
+        at: "02:00:00"
+    condition:
+      - condition: time
+        weekday:
+          - sun  # Every Sunday
+    action:
+      # Full charge cycle
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_charge_limit
+        data:
+          value: 100
+      
+      - delay:
+          hours: 6
+      
+      # Return to normal (90% max)
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_charge_limit
+        data:
+          value: 90
+```
+
+---
+
+## 📊 Dashboard Examples
+
+### Control Panel Card
+
+```yaml
+type: entities
+title: Solar System Controls
+entities:
+  - entity: select.solar_station_xxxxx_operating_mode
+    name: Operating Mode
+  
+  - entity: select.solar_station_xxxxx_battery_priority
+    name: Battery Priority
+  
+  - type: divider
+  
+  - entity: number.solar_station_xxxxx_battery_charge_limit
+    name: Charge Limit
+  
+  - entity: number.solar_station_xxxxx_battery_discharge_limit
+    name: Discharge Limit
+  
+  - entity: number.solar_station_xxxxx_grid_charge_limit
+    name: Grid Charge Power
+  
+  - type: divider
+  
+  - entity: switch.solar_station_xxxxx_grid_charging
+    name: Grid Charging
+  
+  - entity: switch.solar_station_xxxxx_grid_feed_in
+    name: Grid Feed-In
+  
+  - entity: switch.solar_station_xxxxx_backup_mode
+    name: Backup Mode
+```
+
+### Battery Management Card
+
+```yaml
+type: vertical-stack
+cards:
+  - type: gauge
+    entity: sensor.solar_station_xxxxx_battery_state_of_charge
+    name: Battery Level
+    min: 0
+    max: 100
+    needle: true
+    segments:
+      - from: 0
+        color: "#db4437"
+      - from: 20
+        color: "#ff9800"
+      - from: 50
+        color: "#ffc107"
+      - from: 80
+        color: "#0f9d58"
+  
+  - type: entities
+    title: Battery Controls
+    entities:
+      - entity: number.solar_station_xxxxx_battery_charge_limit
+        name: Max Charge
+      - entity: number.solar_station_xxxxx_battery_discharge_limit
+        name: Min Discharge
+      - entity: switch.solar_station_xxxxx_backup_mode
+        name: Backup Reserve
+```
+
+### Quick Actions Card
+
+```yaml
+type: horizontal-stack
+cards:
+  - type: button
+    name: Self-Use Mode
+    icon: mdi:solar-power
+    tap_action:
+      action: call-service
+      service: select.select_option
+      target:
+        entity_id: select.solar_station_xxxxx_operating_mode
+      data:
+        option: "Self-Use"
+  
+  - type: button
+    name: Backup Mode
+    icon: mdi:battery-lock
+    tap_action:
+      action: call-service
+      service: switch.turn_on
+      target:
+        entity_id: switch.solar_station_xxxxx_backup_mode
+  
+  - type: button
+    name: Grid Export
+    icon: mdi:transmission-tower-export
+    tap_action:
+      action: call-service
+      service: switch.toggle
+      target:
+        entity_id: switch.solar_station_xxxxx_grid_feed_in
+```
+
+---
+
+## ⚠️ Important Notes
+
+### API Limitations
+
+**Note**: The control API endpoints (`/api/device/settings/v1` and `/api/device/settings/update/v1`) are **assumed endpoints** based on common IoT patterns. You will need to:
+
+1. **Verify** these endpoints exist in the actual Solar of Things API
+2. **Test** with your device to confirm functionality
+3. **Adjust** the API implementation based on actual response formats
+
+### Finding Actual Control Endpoints
+
+To find the real control endpoints:
+
+1. Open https://solar.siseli.com
+2. Go to settings/control page on the web interface
+3. Open browser Developer Tools (F12) → Network tab
+4. Make a settings change (e.g., adjust battery limit)
+5. Find the API request in the Network tab
+6. Note the:
+   - Endpoint URL
+   - Request method (POST/PUT)
+   - Payload format
+   - Response format
+
+Then update `api.py` with the correct endpoints and formats.
+
+### Safety Considerations
+
+⚠️ **Always test control features carefully:**
+
+- Start with non-critical settings
+- Monitor system behavior after changes
+- Keep battery discharge limits reasonable (don't go below 10-20%)
+- Don't exceed manufacturer specifications
+- Have manual override capability
+
+### Permissions
+
+Some settings may require:
+- Admin/installer level access
+- Special authentication
+- Firmware version requirements
+
+Check your Solar of Things account permissions.
+
+---
+
+## 🔧 Troubleshooting
+
+### Settings Not Changing
+
+1. **Check logs**: Settings → System → Logs
+2. **Verify device_id**: Must be correct for control operations
+3. **Test API**: Try changing settings on solar.siseli.com web interface
+4. **Check permissions**: Account may not have control access
+
+### Control Entities Not Appearing
+
+1. **Check device_id**: Required for control entities
+2. **Restart HA**: After integration update
+3. **Check coordinator**: Ensure settings are being fetched
+4. **Review logs**: Look for error messages
+
+### Values Not Updating
+
+- Wait 5 minutes for next coordinator refresh
+- Or restart Home Assistant to force update
+- Check if web interface shows updated values
+
+---
+
+## 📝 Customization
+
+You can customize available options by editing the integration files:
+
+**Operating Modes** (`select.py`):
+```python
+OPERATING_MODES = [
+    "Self-Use",
+    "Time-of-Use",
+    "Backup",
+    "Grid-Tie",
+    "Off-Grid",
+    # Add more modes here
+]
+```
+
+**Battery Priority** (`select.py`):
+```python
+BATTERY_PRIORITY_MODES = [
+    "Solar First",
+    "Battery First",
+    "Grid First",
+    # Add more priorities here
+]
+```
+
+---
+
+## 🚀 Advanced Usage
+
+### Script for Quick Mode Switching
+
+```yaml
+script:
+  solar_storm_mode:
+    alias: "Solar: Storm Mode"
+    sequence:
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_backup_mode
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_discharge_limit
+        data:
+          value: 50
+      - service: switch.turn_on
+        target:
+          entity_id: switch.solar_station_xxxxx_grid_charging
+      - service: notify.mobile_app
+        data:
+          message: "Solar system in storm protection mode"
+  
+  solar_normal_mode:
+    alias: "Solar: Normal Mode"
+    sequence:
+      - service: switch.turn_off
+        target:
+          entity_id: switch.solar_station_xxxxx_backup_mode
+      - service: number.set_value
+        target:
+          entity_id: number.solar_station_xxxxx_battery_discharge_limit
+        data:
+          value: 10
+      - service: select.select_option
+        target:
+          entity_id: select.solar_station_xxxxx_operating_mode
+        data:
+          option: "Self-Use"
+```
+
+### Scene Integration
+
+```yaml
+scene:
+  - name: Solar Economy Mode
+    entities:
+      select.solar_station_xxxxx_operating_mode: "Time-of-Use"
+      select.solar_station_xxxxx_battery_priority: "Grid First"
+      switch.solar_station_xxxxx_grid_feed_in: "on"
+  
+  - name: Solar Backup Mode
+    entities:
+      select.solar_station_xxxxx_operating_mode: "Backup"
+      switch.solar_station_xxxxx_backup_mode: "on"
+      number.solar_station_xxxxx_battery_discharge_limit: 50
+```
+
+---
+
+**Full control of your solar system is now at your fingertips!** 🎉

+ 382 - 0
EXAMPLES.md

@@ -0,0 +1,382 @@
+# Example Configurations
+
+## Basic Lovelace Cards
+
+### Simple Status Card
+
+```yaml
+type: entities
+title: Solar System Status
+entities:
+  - entity: sensor.solar_station_xxxxx_pv_input_power
+    name: Solar Generation
+    icon: mdi:solar-power
+  - entity: sensor.solar_station_xxxxx_battery_state_of_charge
+    name: Battery
+    icon: mdi:battery
+  - entity: sensor.solar_station_xxxxx_load_power
+    name: Load
+    icon: mdi:home-lightning-bolt
+  - entity: sensor.solar_station_xxxxx_grid_import_power
+    name: Grid Import
+    icon: mdi:transmission-tower-import
+```
+
+### Gauge Cards
+
+```yaml
+type: vertical-stack
+cards:
+  - type: gauge
+    entity: sensor.solar_station_xxxxx_battery_state_of_charge
+    name: Battery Level
+    min: 0
+    max: 100
+    needle: true
+    severity:
+      green: 60
+      yellow: 30
+      red: 0
+  
+  - type: gauge
+    entity: sensor.solar_station_xxxxx_pv_input_power
+    name: Solar Generation
+    min: 0
+    max: 5000
+    needle: true
+    unit: W
+```
+
+### Power Flow Card (Custom Card Required)
+
+First install the Power Flow Card from HACS, then:
+
+```yaml
+type: custom:power-flow-card
+entities:
+  solar: sensor.solar_station_xxxxx_pv_input_power
+  battery: sensor.solar_station_xxxxx_battery_power
+  grid: sensor.solar_station_xxxxx_grid_import_power
+  home: sensor.solar_station_xxxxx_load_power
+  battery_charge: sensor.solar_station_xxxxx_battery_state_of_charge
+```
+
+### History Graph
+
+```yaml
+type: history-graph
+title: Solar Production (24h)
+hours_to_show: 24
+entities:
+  - entity: sensor.solar_station_xxxxx_pv_input_power
+    name: Solar
+  - entity: sensor.solar_station_xxxxx_load_power
+    name: Load
+  - entity: sensor.solar_station_xxxxx_grid_import_power
+    name: Grid
+```
+
+### Statistics Card
+
+```yaml
+type: statistics-graph
+title: Monthly Solar Statistics
+entities:
+  - entity: sensor.solar_station_xxxxx_pv_input_power
+stat_types:
+  - mean
+  - max
+  - min
+period:
+  calendar:
+    period: month
+```
+
+## Automations
+
+### Low Battery Alert
+
+```yaml
+automation:
+  - alias: "Solar Battery Low"
+    description: "Alert when battery drops below 20%"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.solar_station_xxxxx_battery_state_of_charge
+        below: 20
+    condition:
+      - condition: state
+        entity_id: sun.sun
+        state: "below_horizon"
+    action:
+      - service: notify.mobile_app
+        data:
+          title: "Solar Battery Low"
+          message: "Battery is at {{ states('sensor.solar_station_xxxxx_battery_state_of_charge') }}%"
+          data:
+            priority: high
+```
+
+### High Solar Production Notification
+
+```yaml
+automation:
+  - alias: "Solar Production Peak"
+    description: "Notify when solar production exceeds threshold"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.solar_station_xxxxx_pv_input_power
+        above: 4000
+        for:
+          minutes: 10
+    action:
+      - service: notify.mobile_app
+        data:
+          message: "Excellent solar production: {{ states('sensor.solar_station_xxxxx_pv_input_power') }}W!"
+```
+
+### Grid Import Alert
+
+```yaml
+automation:
+  - alias: "High Grid Usage"
+    description: "Alert when importing significant power from grid"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.solar_station_xxxxx_grid_import_power
+        above: 2000
+        for:
+          minutes: 15
+    condition:
+      - condition: state
+        entity_id: sun.sun
+        state: "above_horizon"
+    action:
+      - service: notify.mobile_app
+        data:
+          message: "High grid usage ({{ states('sensor.solar_station_xxxxx_grid_import_power') }}W) despite daylight. Check system."
+```
+
+### Daily Solar Summary
+
+```yaml
+automation:
+  - alias: "Daily Solar Report"
+    description: "Send daily solar production summary"
+    trigger:
+      - platform: time
+        at: "21:00:00"
+    action:
+      - service: notify.mobile_app
+        data:
+          title: "Today's Solar Report"
+          message: |
+            PV Generated: {{ states('sensor.solar_station_xxxxx_monthly_pv_generated') }} kWh this month
+            Current Battery: {{ states('sensor.solar_station_xxxxx_battery_state_of_charge') }}%
+            Peak Production: {{ state_attr('sensor.solar_station_xxxxx_pv_input_power', 'max_value') }}W
+```
+
+### Smart Device Control
+
+```yaml
+automation:
+  - alias: "Run Pool Pump on Solar"
+    description: "Run pool pump when excess solar available"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.solar_station_xxxxx_pv_input_power
+        above: 3000
+    condition:
+      - condition: numeric_state
+        entity_id: sensor.solar_station_xxxxx_battery_state_of_charge
+        above: 80
+      - condition: numeric_state
+        entity_id: sensor.solar_station_xxxxx_grid_import_power
+        below: 100
+      - condition: time
+        after: "10:00:00"
+        before: "16:00:00"
+    action:
+      - service: switch.turn_on
+        entity_id: switch.pool_pump
+      - delay:
+          hours: 2
+      - service: switch.turn_off
+        entity_id: switch.pool_pump
+```
+
+## Scripts
+
+### Calculate Daily Energy
+
+```yaml
+script:
+  calculate_daily_solar:
+    alias: "Calculate Daily Solar Energy"
+    sequence:
+      - service: utility_meter.calibrate
+        data:
+          entity_id: sensor.daily_solar_energy
+          value: "{{ states('sensor.solar_station_xxxxx_pv_input_power') | float }}"
+```
+
+## Template Sensors
+
+Add to your `configuration.yaml`:
+
+```yaml
+template:
+  - sensor:
+      - name: "Solar Self Consumption Rate"
+        unique_id: solar_self_consumption_rate
+        unit_of_measurement: "%"
+        state: >
+          {% set pv = states('sensor.solar_station_xxxxx_pv_input_power') | float(0) %}
+          {% set grid = states('sensor.solar_station_xxxxx_grid_import_power') | float(0) %}
+          {% set load = states('sensor.solar_station_xxxxx_load_power') | float(0) %}
+          {% if load > 0 %}
+            {{ ((load - grid) / load * 100) | round(1) }}
+          {% else %}
+            0
+          {% endif %}
+        icon: mdi:percent
+
+      - name: "Solar Battery Status"
+        unique_id: solar_battery_status
+        state: >
+          {% set soc = states('sensor.solar_station_xxxxx_battery_state_of_charge') | float(0) %}
+          {% if soc > 80 %}
+            Full
+          {% elif soc > 50 %}
+            Good
+          {% elif soc > 20 %}
+            Medium
+          {% else %}
+            Low
+          {% endif %}
+        icon: >
+          {% set soc = states('sensor.solar_station_xxxxx_battery_state_of_charge') | float(0) %}
+          {% if soc > 80 %}
+            mdi:battery
+          {% elif soc > 50 %}
+            mdi:battery-70
+          {% elif soc > 20 %}
+            mdi:battery-30
+          {% else %}
+            mdi:battery-10
+          {% endif %}
+
+      - name: "Solar Power Balance"
+        unique_id: solar_power_balance
+        unit_of_measurement: "W"
+        state: >
+          {% set pv = states('sensor.solar_station_xxxxx_pv_input_power') | float(0) %}
+          {% set load = states('sensor.solar_station_xxxxx_load_power') | float(0) %}
+          {{ (pv - load) | round(0) }}
+        icon: mdi:scale-balance
+```
+
+## Energy Dashboard Configuration
+
+### Configure Energy Sources
+
+1. Go to **Settings** → **Dashboards** → **Energy**
+
+2. **Grid Consumption**:
+   - Click "Add Consumption"
+   - Select: `sensor.solar_station_xxxxx_grid_import_power`
+   - Use an integration sensor (Riemann sum) if needed
+
+3. **Return to Grid**:
+   - Click "Add Grid Return"
+   - Select: `sensor.solar_station_xxxxx_grid_feed_in_power`
+
+4. **Solar Panels**:
+   - Click "Add Solar Production"
+   - Select: `sensor.solar_station_xxxxx_pv_input_power`
+
+5. **Battery**:
+   - Click "Add Battery System"
+   - Energy going in: `sensor.solar_station_xxxxx_battery_charging_current`
+   - Energy going out: `sensor.solar_station_xxxxx_battery_discharge_current`
+
+### Create Integration Sensors
+
+Add to `configuration.yaml` to track total energy:
+
+```yaml
+sensor:
+  - platform: integration
+    source: sensor.solar_station_xxxxx_pv_input_power
+    name: Solar Energy Total
+    unit_prefix: k
+    round: 2
+    
+  - platform: integration
+    source: sensor.solar_station_xxxxx_grid_import_power
+    name: Grid Import Energy Total
+    unit_prefix: k
+    round: 2
+    
+  - platform: integration
+    source: sensor.solar_station_xxxxx_grid_feed_in_power
+    name: Grid Export Energy Total
+    unit_prefix: k
+    round: 2
+```
+
+## Custom Dashboard Example
+
+Complete dashboard for comprehensive solar monitoring:
+
+```yaml
+title: Solar System
+views:
+  - title: Overview
+    cards:
+      - type: vertical-stack
+        cards:
+          - type: entities
+            title: Current Status
+            entities:
+              - sensor.solar_station_xxxxx_pv_input_power
+              - sensor.solar_station_xxxxx_battery_state_of_charge
+              - sensor.solar_station_xxxxx_load_power
+              - sensor.solar_station_xxxxx_grid_import_power
+          
+          - type: history-graph
+            title: Power Flow (24h)
+            hours_to_show: 24
+            entities:
+              - sensor.solar_station_xxxxx_pv_input_power
+              - sensor.solar_station_xxxxx_load_power
+              - sensor.solar_station_xxxxx_grid_import_power
+          
+          - type: gauge
+            entity: sensor.solar_station_xxxxx_battery_state_of_charge
+            min: 0
+            max: 100
+            
+  - title: Battery
+    cards:
+      - type: entities
+        title: Battery Details
+        entities:
+          - sensor.solar_station_xxxxx_battery_voltage
+          - sensor.solar_station_xxxxx_battery_charging_current
+          - sensor.solar_station_xxxxx_battery_discharge_current
+          - sensor.solar_station_xxxxx_battery_power
+          
+  - title: Statistics
+    cards:
+      - type: statistics-graph
+        title: This Month
+        entities:
+          - sensor.solar_station_xxxxx_pv_input_power
+        stat_types:
+          - mean
+          - max
+        period:
+          calendar:
+            period: month
+```

+ 244 - 0
INSTALLATION.md

@@ -0,0 +1,244 @@
+# Installation Guide
+
+## Prerequisites
+
+- Home Assistant (2023.1 or newer recommended)
+- Access to your Home Assistant configuration directory
+- Solar of Things account with at least one configured station
+
+## Step-by-Step Installation
+
+### Method 1: HACS Installation (Recommended)
+
+1. **Install HACS** (if not already installed)
+   - Follow the official HACS installation guide: https://hacs.xyz/docs/setup/download
+
+2. **Add Custom Repository**
+   - Open HACS in your Home Assistant interface
+   - Click on "Integrations"
+   - Click the three dots (⋮) in the top right corner
+   - Select "Custom repositories"
+   - Add repository URL: `https://github.com/conexocasa/solar-of-things-ha`
+   - Category: Integration
+   - Click "Add"
+
+3. **Install the Integration**
+   - Find "Solar of Things" in HACS
+   - Click "Download"
+   - Restart Home Assistant
+
+4. **Configure the Integration**
+   - Go to Settings → Devices & Services
+   - Click "+ Add Integration"
+   - Search for "Solar of Things"
+   - Follow the configuration steps
+
+### Method 2: Manual Installation
+
+1. **Download the Integration**
+   ```bash
+   # SSH into your Home Assistant instance or use the File Editor add-on
+   cd /config/custom_components/
+   git clone https://github.com/conexocasa/solar-of-things-ha.git solar_of_things
+   ```
+
+   Or download and extract the ZIP file:
+   - Download the latest release from GitHub
+   - Extract the `solar_of_things` folder
+   - Copy it to `/config/custom_components/`
+
+2. **Verify Installation**
+   Your directory structure should look like:
+   ```
+   /config/
+   └── custom_components/
+       └── solar_of_things/
+           ├── __init__.py
+           ├── api.py
+           ├── config_flow.py
+           ├── const.py
+           ├── manifest.json
+           ├── sensor.py
+           ├── strings.json
+           └── translations/
+               └── en.json
+   ```
+
+3. **Restart Home Assistant**
+   - Go to Settings → System → Restart
+   - Or use the command: `ha core restart`
+
+4. **Add the Integration**
+   - Go to Settings → Devices & Services
+   - Click "+ Add Integration"
+   - Search for "Solar of Things"
+   - Enter your credentials
+
+## Finding Your Credentials
+
+### IOT Token
+
+1. Open https://solar.siseli.com in your browser
+2. Log in to your account
+3. Press `F12` to open Developer Tools
+4. Click on the **Network** tab
+5. Refresh the page (`Ctrl+R` or `Cmd+R`)
+6. Look for API requests (you can filter by typing "api" in the filter box)
+7. Click on any request and look at the **Request Headers** section
+8. Find `IOT-Token` and copy its value
+
+**Example:**
+```
+IOT-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+```
+
+### Device ID and Station ID
+
+While still in the Network tab:
+
+1. Look for requests to endpoints like:
+   - `/apis/device/list`
+   - `/apis/deviceState/simple/attribute/keys/history/v1`
+   - `/apis/stationOverView/stateAttributeSummary/category/yearly`
+
+2. Click on a request and look at the **Payload** or **Request** section
+
+3. Find values like:
+   ```json
+   {
+     "deviceId": "123456789012345678",
+     "stationId": "987654321098765432"
+   }
+   ```
+
+**Note:** These are 18-digit numbers. Make sure to copy them exactly!
+
+## Configuration Options
+
+When adding the integration, you'll be prompted for:
+
+### Required:
+- **IOT Token**: Your authentication token from the Siseli portal
+
+### Required:
+- **Station ID**: Required (used to auto-discover your device IDs)
+
+### Optional:
+- **Device ID**: Optional; set it only if you want to restrict Home Assistant entities to a single device under the station
+
+### Update Interval:
+- Default: 5 minutes
+- Can be changed in integration options (Settings → Devices & Services → Solar of Things → Configure)
+- Recommended range: 1-60 minutes
+
+## Adding Multiple Stations
+
+If you have multiple solar stations:
+
+1. Add the integration once for the first station
+2. Go back to Settings → Devices & Services
+3. Click "+ Add Integration" again
+4. Search for "Solar of Things"
+5. Enter the credentials for your second station
+6. Repeat for each additional station
+
+Each station will appear as a separate device in Home Assistant.
+
+## Verifying Installation
+
+After installation and configuration:
+
+1. Go to **Settings** → **Devices & Services**
+2. Find "Solar of Things" in the integrations list
+3. Click on it to see your configured stations
+4. Click on a station to see all available sensors
+5. Sensors should start showing data within 5-10 minutes
+
+### Expected Sensors
+
+For each configured station, you should see:
+
+**Real-time sensors** (if Device ID provided):
+- PV Input Power
+- AC Output Power
+- Battery Discharge Current
+- Battery Charging Current
+- Battery Voltage
+- Battery Power
+- Battery State of Charge
+- Grid Feed-in Power
+- Grid Import Power
+- Load Power
+
+**Monthly sensors** (if Station ID provided):
+- Monthly PV Generated
+
+## Troubleshooting Installation
+
+### Integration Not Showing Up
+
+1. **Check file structure**: Ensure all files are in `/config/custom_components/solar_of_things/`
+2. **Check file permissions**: Files should be readable by the Home Assistant user
+3. **Check logs**: Go to Settings → System → Logs and search for "solar_of_things"
+4. **Clear browser cache**: Sometimes the UI needs a hard refresh (`Ctrl+Shift+R`)
+
+### "Cannot Connect" Error
+
+- Verify your IOT Token hasn't expired (tokens may expire after some time)
+- Check your Device ID and Station ID are correct (18-digit numbers)
+- Ensure Home Assistant can reach https://solar.siseli.com (check firewall/network)
+- Get a fresh IOT Token from the Siseli portal
+
+### Sensors Not Updating
+
+1. Wait at least 5-10 minutes after initial configuration
+2. Check if your solar system is online at solar.siseli.com
+3. Verify the update interval in integration options
+4. Check Home Assistant logs for errors
+
+### Permission Denied Errors
+
+If you see permission errors in logs:
+```bash
+# SSH into Home Assistant
+cd /config/custom_components/
+chmod -R 755 solar_of_things/
+chown -R homeassistant:homeassistant solar_of_things/
+```
+
+## Uninstalling
+
+To remove the integration:
+
+1. Go to Settings → Devices & Services
+2. Find "Solar of Things"
+3. Click the three dots (⋮) next to the integration
+4. Select "Delete"
+5. Confirm deletion
+
+To completely remove the files:
+```bash
+rm -rf /config/custom_components/solar_of_things/
+```
+
+Then restart Home Assistant.
+
+## Next Steps
+
+After successful installation:
+
+1. [Configure Energy Dashboard](README.md#energy-dashboard)
+2. [Create automations](README.md#automation-example)
+3. [Add Lovelace cards](README.md#lovelace-card-example)
+4. Set up notifications for low battery or high solar production
+
+## Getting Help
+
+If you encounter issues:
+
+1. Check the [Troubleshooting section](README.md#troubleshooting) in README
+2. Review Home Assistant logs for error messages
+3. Open an issue on GitHub with:
+   - Your Home Assistant version
+   - Error messages from logs
+   - Steps to reproduce the problem

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Solar of Things Home Assistant Integration Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 106 - 0
QUICKSTART.md

@@ -0,0 +1,106 @@
+# Quick Start Guide
+
+Get your Solar of Things integration running in 5 minutes!
+
+## Prerequisites
+
+- ✅ Home Assistant installed (2023.1+)
+- ✅ Access to https://solar.siseli.com
+- ✅ At least one solar station configured
+
+## Step 1: Get Your Credentials (2 minutes)
+
+1. Open https://solar.siseli.com in your browser
+2. Login to your account
+3. Press **F12** to open Developer Tools
+4. Click the **Network** tab
+5. Refresh the page (**Ctrl+R** or **Cmd+R**)
+6. Click on any API request
+7. Copy these values:
+   - **IOT-Token** from Request Headers
+   - **deviceId** from Request Payload (18 digits)
+   - **stationId** from Request Payload (18 digits)
+
+![Finding Credentials](https://via.placeholder.com/600x300?text=See+INSTALLATION.md+for+detailed+screenshots)
+
+## Step 2: Install Integration (1 minute)
+
+### Option A: HACS (Recommended)
+1. Open HACS → Integrations
+2. Click ⋮ → Custom repositories
+3. Add: `https://github.com/conexocasa/solar-of-things-ha`
+4. Install "Solar of Things"
+
+### Option B: Manual
+1. Download this repository
+2. Copy `solar_of_things` folder to `/config/custom_components/`
+3. Restart Home Assistant
+
+## Step 3: Configure (2 minutes)
+
+1. Go to **Settings** → **Devices & Services**
+2. Click **+ Add Integration**
+3. Search for **"Solar of Things"**
+4. Enter your credentials:
+   ```
+   IOT Token:  [paste your token]
+   Station ID: [your 18-digit station ID]
+   Device ID:  [optional; leave blank to auto-discover all devices]
+   ```
+5. Click **Submit**
+
+## Step 4: Verify (30 seconds)
+
+1. Go to **Settings** → **Devices & Services**
+2. Find your "Solar Station" device
+3. You should see sensors like:
+   - ✅ PV Input Power
+   - ✅ Battery State of Charge
+   - ✅ Grid Import Power
+   - ✅ Load Power
+
+## Step 5: Add to Dashboard (Optional)
+
+### Quick Dashboard Card
+
+```yaml
+type: entities
+title: My Solar System
+entities:
+  - sensor.solar_station_xxxxx_pv_input_power
+  - sensor.solar_station_xxxxx_battery_state_of_charge
+  - sensor.solar_station_xxxxx_load_power
+```
+
+See [EXAMPLES.md](EXAMPLES.md) for more advanced cards!
+
+## Troubleshooting
+
+### "Cannot Connect" Error
+- ❌ Token expired → Get fresh token from solar.siseli.com
+- ❌ Wrong Device/Station ID → Verify 18-digit numbers
+- ❌ Network issue → Check firewall/internet
+
+### No Sensors Showing
+- ⏱️ Wait 5-10 minutes for first update
+- 🔍 Check logs: Settings → System → Logs
+- 🌐 Verify device online at solar.siseli.com
+
+### Need Help?
+- 📖 [Full Documentation](README.md)
+- 🛠️ [Installation Guide](INSTALLATION.md)
+- 💡 [Examples](EXAMPLES.md)
+- 🐛 [Report Issue](https://github.com/conexocasa/solar-of-things-ha/issues)
+
+## Next Steps
+
+- [ ] Add to Energy Dashboard
+- [ ] Create automations for low battery alerts
+- [ ] Set up notifications for high solar production
+- [ ] Explore template sensors for custom calculations
+
+See [EXAMPLES.md](EXAMPLES.md) for ideas!
+
+---
+
+**🎉 Congratulations!** You're now monitoring your solar system in Home Assistant!

+ 639 - 0
README.md

@@ -0,0 +1,639 @@
+# ☀️ Solar of Things — Home Assistant Integration
+
+<p align="center">
+  <a href="https://github.com/conexocasa/solar-of-things-ha/releases/latest">
+    <img src="https://img.shields.io/github/v/release/conexocasa/solar-of-things-ha?style=for-the-badge&label=Latest%20Release&color=orange" alt="Latest Release">
+  </a>
+  <a href="https://github.com/custom-components/hacs">
+    <img src="https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge" alt="HACS Custom">
+  </a>
+  <a href="https://www.home-assistant.io/">
+    <img src="https://img.shields.io/badge/Home%20Assistant-2023.1%2B-41BDF5?style=for-the-badge&logo=home-assistant" alt="Home Assistant">
+  </a>
+  <a href="LICENSE">
+    <img src="https://img.shields.io/github/license/conexocasa/solar-of-things-ha?style=for-the-badge" alt="MIT License">
+  </a>
+  <a href="https://github.com/conexocasa/solar-of-things-ha/actions/workflows/validate.yml">
+    <img src="https://img.shields.io/github/actions/workflow/status/conexocasa/solar-of-things-ha/validate.yml?branch=main&style=for-the-badge&label=CI" alt="CI Status">
+  </a>
+</p>
+
+<p align="center">
+  <strong>Monitor and control your Siseli solar inverter directly from Home Assistant.</strong><br>
+  Real-time power data · Battery management · Grid control · Energy Dashboard ready
+</p>
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Features](#features)
+- [Sensors](#sensors)
+- [Control Entities](#control-entities)
+- [Requirements](#requirements)
+- [Installation](#installation)
+  - [HACS (Recommended)](#hacs-recommended)
+  - [Manual Installation](#manual-installation)
+- [Configuration](#configuration)
+  - [Step 1 — Find Your Credentials](#step-1--find-your-credentials)
+  - [Step 2 — Add the Integration](#step-2--add-the-integration)
+  - [Step 3 — Verify Entities](#step-3--verify-entities)
+  - [Multiple Stations](#multiple-stations)
+- [Screenshots](#screenshots)
+- [Energy Dashboard](#energy-dashboard)
+- [Automation Examples](#automation-examples)
+- [Dashboard Card Examples](#dashboard-card-examples)
+- [Troubleshooting](#troubleshooting)
+- [API Reference](#api-reference)
+- [Contributing](#contributing)
+- [Changelog](#changelog)
+- [License](#license)
+
+---
+
+## Overview
+
+The **Solar of Things** integration connects Home Assistant to the
+[Siseli solar portal](https://solar.siseli.com) and provides:
+
+- **Auto-discovery** of every inverter device under your station — enter your
+  Station ID once and Home Assistant finds all your devices automatically.
+- **10+ real-time sensors** for power, battery, grid and load data, updated
+  every 5 minutes.
+- **4 monthly summary sensors** for energy totals and solar coverage.
+- **8 control entities** (sliders, dropdowns, switches) to manage battery
+  limits, operating modes and grid settings directly from HA.
+- Full compatibility with the **Home Assistant Energy Dashboard**.
+- **Multi-station support** — add the integration once per station.
+
+> API behavior is based on the Siseli web portal and the reference CLI client
+> at [Hyllesen/solar-of-things-solar-usage](https://github.com/Hyllesen/solar-of-things-solar-usage).
+
+---
+
+## Features
+
+| Category | What you get |
+|---|---|
+| 🔍 **Auto-discovery** | Enter Station ID → HA fetches all device IDs automatically |
+| 📊 **Real-time monitoring** | 10 per-device sensors, updated every 5 min |
+| 📅 **Monthly statistics** | 4 station-level energy summary sensors |
+| 🎛️ **System control** | 8 control entities (battery limits, modes, grid switches) |
+| ⚡ **Energy Dashboard** | All power and energy sensors are dashboard-ready |
+| 🏠 **Multi-station** | Unlimited stations via multiple config entries |
+| 🔒 **Secure auth** | IOT-Token header authentication (no username/password stored) |
+| 🌏 **Timezone-aware** | Configurable `IOT-Time-Zone` header per integration |
+| 🔄 **Auto-retry** | HA coordinator pattern with automatic retry on failure |
+
+---
+
+## Sensors
+
+### Real-time Device Sensors
+> Updated every **5 minutes** · One set per discovered device
+
+| Entity | Unit | Device Class | Description |
+|---|---|---|---|
+| `{device} PV Input Power` | W | `power` | Solar panel DC input power |
+| `{device} AC Output Power` | W | `power` | AC power delivered to loads |
+| `{device} Battery Charging Current` | A | `current` | Current flowing into battery |
+| `{device} Battery Discharge Current` | A | `current` | Current flowing out of battery |
+| `{device} Battery Voltage` | V | `voltage` | Battery bank terminal voltage |
+| `{device} Battery Power` | W | `power` | Net battery power (calculated: discharge − charge × voltage) |
+| `{device} Battery State of Charge` | % | `battery` | Battery charge level |
+| `{device} Grid Feed-in Power` | W | `power` | Power exported to the utility grid |
+| `{device} Grid Import Power` | W | `power` | Power imported from the utility grid (calculated) |
+| `{device} Load Power` | W | `power` | Total household / load consumption |
+
+### Monthly Station Sensors
+> Updated every **30 minutes** · Requires Station ID
+
+| Entity | Unit | Device Class | Description |
+|---|---|---|---|
+| `Station {id} Monthly PV Generated` | kWh | `energy` | Total solar generation this month |
+| `Station {id} Monthly Grid Import` | kWh | `energy` | Total grid import this month |
+| `Station {id} Monthly Total Consumption` | kWh | `energy` | Total household consumption this month |
+| `Station {id} Monthly Solar Coverage` | % | — | Percentage of consumption met by solar |
+
+---
+
+## Control Entities
+
+> Control entities require your device firmware/account to support the
+> settings API. If controls are unresponsive, see
+> [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
+
+### Number Entities (Sliders)
+
+| Entity | Range | Step | Unit | Description |
+|---|---|---|---|---|
+| `{device} Battery Charge Limit` | 0 – 100 | 1 | % | Maximum SOC target for charging |
+| `{device} Battery Discharge Limit` | 0 – 100 | 1 | % | Minimum SOC before stopping discharge |
+| `{device} Grid Charge Limit` | 0 – 5000 | 100 | W | Max grid power used for battery charging |
+
+### Select Entities (Dropdowns)
+
+| Entity | Options | Description |
+|---|---|---|
+| `{device} Operating Mode` | Self-Use · Time-of-Use · Backup · Grid-Tie · Off-Grid | System operating strategy |
+| `{device} Battery Priority` | Solar First · Battery First · Grid First | Energy source priority order |
+
+### Switch Entities
+
+| Entity | Description |
+|---|---|
+| `{device} Grid Charging` | Allow/deny battery charging from the grid |
+| `{device} Grid Feed-In` | Allow/deny exporting excess power to the grid |
+| `{device} Backup Mode` | Reserve battery capacity for power outages |
+
+---
+
+## Requirements
+
+| Requirement | Details |
+|---|---|
+| Home Assistant | 2023.1 or newer |
+| Siseli account | Active account at [solar.siseli.com](https://solar.siseli.com) |
+| IOT-Token | Retrieved from browser DevTools (see [Configuration](#configuration)) |
+| Station ID | 18-digit ID from the Siseli portal |
+| Network | Home Assistant must be able to reach `https://solar.siseli.com` |
+
+---
+
+## Installation
+
+### HACS (Recommended)
+
+> HACS is the easiest way to install and keep the integration up to date.
+
+1. Open **HACS** in your Home Assistant sidebar.
+2. Click **Integrations**.
+3. Click the three-dot menu **⋮** (top-right) → **Custom repositories**.
+4. Add the repository:
+   ```
+   https://github.com/conexocasa/solar-of-things-ha
+   ```
+   Category: **Integration**
+5. Click **Add**, then search for **Solar of Things**.
+6. Click **Download** and confirm.
+7. **Restart Home Assistant.**
+
+> After restarting, continue to [Configuration](#configuration).
+
+---
+
+### Manual Installation
+
+1. Download the latest release asset:
+   **[solar-of-things-ha.zip](https://github.com/conexocasa/solar-of-things-ha/releases/latest)**
+
+2. Extract the ZIP. Inside you will find:
+   ```
+   custom_components/
+   └── solar_of_things/
+       ├── __init__.py
+       ├── api.py
+       ├── config_flow.py
+       ├── const.py
+       ├── manifest.json
+       ├── number.py
+       ├── select.py
+       ├── sensor.py
+       ├── strings.json
+       ├── switch.py
+       └── translations/
+           └── en.json
+   ```
+
+3. Copy the **`solar_of_things`** folder into your HA config directory:
+   ```
+   /config/custom_components/solar_of_things/
+   ```
+
+4. **Restart Home Assistant.**
+
+> Verify the folder is in the right place: go to
+> **Settings → System → Logs** and look for `solar_of_things`.
+> No errors means the component loaded correctly.
+
+---
+
+## Configuration
+
+### Step 1 — Find Your Credentials
+
+You need three values from the Siseli portal.
+All three are visible in the browser's Network tab — no third-party tools needed.
+
+1. Open [https://solar.siseli.com](https://solar.siseli.com) and log in.
+2. Press **F12** (or **Cmd + Option + I** on macOS) to open DevTools.
+3. Click the **Network** tab.
+4. Press **Ctrl + R** (or **Cmd + R**) to refresh the page.
+5. Click any request that goes to `solar.siseli.com` and look at:
+
+| Value | Where to find it | Example |
+|---|---|---|
+| **IOT-Token** | Request **Headers** tab → `IOT-Token` | `CA49A160DBFFE7A827...` |
+| **Station ID** | Request **Payload** tab → `stationId` | `423564214316007425` |
+| **Device ID** | Request **Payload** tab → `deviceId` | `423564214341173249` |
+
+> **Tip:** Filter the Network tab by typing `device/list` or `history` to find
+> the relevant requests faster.
+
+> **Security:** Your IOT-Token is session-based and may expire.
+> If the integration stops working, re-copy a fresh token from the portal.
+
+---
+
+### Step 2 — Add the Integration
+
+1. In Home Assistant go to **Settings → Devices & Services**.
+2. Click **+ Add Integration** (bottom-right).
+3. Search for **Solar of Things** and select it.
+4. Fill in the configuration form:
+
+   | Field | Required | Description |
+   |---|---|---|
+   | **IOT Token** | ✅ Yes | Paste your `IOT-Token` header value |
+   | **Station ID** | ✅ Yes | 18-digit Station ID |
+   | **Device ID** | ⬜ Optional | Leave blank to auto-discover ALL devices |
+   | **Time zone** | ⬜ Optional | Default: `Asia/Manila`. Set to your local tz (e.g. `Europe/Berlin`) |
+
+5. Click **Submit**. The integration will:
+   - Validate your token by calling the Siseli API.
+   - Auto-discover all device IDs under the station.
+   - Create HA entities for every discovered device.
+
+> **Device ID is optional.** If you leave it blank, Home Assistant will call
+> `POST /apis/device/list` and create entities for every inverter under your
+> station automatically. Set `Device ID` only if you want to restrict entities
+> to a single device.
+
+---
+
+### Step 3 — Verify Entities
+
+After adding the integration, go to
+**Settings → Devices & Services → Solar of Things** and click your station.
+
+You should see:
+
+- ☀️ Sensor entities (PV power, battery, grid, load)
+- 🔢 Number sliders (battery charge/discharge limits, grid charge limit)
+- 🔽 Select dropdowns (operating mode, battery priority)
+- 🔘 Switches (grid charging, grid feed-in, backup mode)
+- 📊 Monthly energy summary sensors (if station ID is set)
+
+> Entities may show **Unknown** for the first 5 minutes while the coordinator
+> fetches the first data update.
+
+---
+
+### Multiple Stations
+
+To monitor more than one station:
+
+1. Go to **Settings → Devices & Services**.
+2. Click **+ Add Integration** again.
+3. Search for **Solar of Things** and add it with the credentials for your
+   second station.
+4. Repeat for every additional station.
+
+Each station appears as a separate device group in Home Assistant with its own
+set of sensors and control entities.
+
+---
+
+## Screenshots
+
+> The screenshots below show typical HA entity cards for this integration.
+> Your entity names will include your device name (e.g. `1_Inverter`) instead
+> of the placeholder shown.
+
+### Device sensors in Entities view
+```
+┌─────────────────────────────────────────────────────┐
+│  1_Inverter · Siseli Solar Inverter                 │
+├─────────────────────────────────────────────────────┤
+│  ☀️  PV Input Power              22 W               │
+│  🔌  AC Output Power            324 W               │
+│  🔋  Battery State of Charge     76 %               │
+│  🔋  Battery Voltage           27.2 V               │
+│  ⬆️  Battery Charging Current   1.2 A               │
+│  ⬇️  Battery Discharge Current  0.0 A               │
+│  ⚡  Battery Power             32.6 W               │
+│  🏠  Load Power                324 W               │
+│  📤  Grid Feed-in Power          0 W               │
+│  📥  Grid Import Power           0 W               │
+├─────────────────────────────────────────────────────┤
+│  🎛️  Battery Charge Limit       ████░░░░  80 %      │
+│  🎛️  Battery Discharge Limit    █░░░░░░░  10 %      │
+│  🎛️  Grid Charge Limit          ░░░░░░░░   0 W      │
+│  🔽  Operating Mode             Self-Use            │
+│  🔽  Battery Priority           Solar First         │
+│  🔘  Grid Charging              OFF                 │
+│  🔘  Grid Feed-In               ON                  │
+│  🔘  Backup Mode                OFF                 │
+└─────────────────────────────────────────────────────┘
+```
+
+### Monthly station summary sensors
+```
+┌─────────────────────────────────────────────────────┐
+│  Solar Station 423564214316007425                   │
+├─────────────────────────────────────────────────────┤
+│  📊  Monthly PV Generated      95.45 kWh            │
+│  📊  Monthly Grid Import        0.00 kWh            │
+│  📊  Monthly Total Consumption  0.00 kWh            │
+│  📊  Monthly Solar Coverage       — %               │
+└─────────────────────────────────────────────────────┘
+```
+
+### Energy Dashboard configuration
+```
+  Solar production  ──▶  sensor.1_inverter_pv_input_power
+  Grid consumption  ──▶  sensor.1_inverter_grid_import_power
+  Return to grid    ──▶  sensor.1_inverter_grid_feed_in_power
+  Battery charge    ──▶  sensor.1_inverter_battery_charging_current
+  Battery discharge ──▶  sensor.1_inverter_battery_discharge_current
+```
+
+---
+
+## Energy Dashboard
+
+This integration is fully compatible with the HA Energy Dashboard.
+
+Go to **Settings → Dashboards → Energy** and add:
+
+| Dashboard slot | Entity to use |
+|---|---|
+| Solar production | `sensor.{device}_pv_input_power` |
+| Grid consumption | `sensor.{device}_grid_import_power` |
+| Return to grid | `sensor.{device}_grid_feed_in_power` |
+| Battery charge | `sensor.{device}_battery_charging_current` |
+| Battery discharge | `sensor.{device}_battery_discharge_current` |
+
+For monthly totals use the station-level sensors:
+
+| Dashboard slot | Entity to use |
+|---|---|
+| Solar production (monthly) | `sensor.station_{id}_monthly_pv_generated` |
+| Grid import (monthly) | `sensor.station_{id}_monthly_grid_import` |
+
+---
+
+## Automation Examples
+
+### Low battery alert
+```yaml
+automation:
+  - alias: "Solar — Low Battery Alert"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.1_inverter_battery_state_of_charge
+        below: 20
+    action:
+      - service: notify.mobile_app
+        data:
+          title: "⚠️ Solar Battery Low"
+          message: >
+            Battery is at
+            {{ states('sensor.1_inverter_battery_state_of_charge') }}%.
+            Consider reducing load or enabling grid charging.
+```
+
+### Auto-enable grid charging at night
+```yaml
+automation:
+  - alias: "Solar — Enable Grid Charging at Night"
+    trigger:
+      - platform: time
+        at: "22:00:00"
+    action:
+      - service: switch.turn_on
+        target:
+          entity_id: switch.1_inverter_grid_charging
+```
+
+### Switch to backup mode before a storm
+```yaml
+automation:
+  - alias: "Solar — Backup Mode Before Storm"
+    trigger:
+      - platform: state
+        entity_id: weather.home
+        to: "rainy"
+    condition:
+      - condition: numeric_state
+        entity_id: sensor.1_inverter_battery_state_of_charge
+        below: 80
+    action:
+      - service: switch.turn_on
+        target:
+          entity_id: switch.1_inverter_backup_mode
+      - service: number.set_value
+        target:
+          entity_id: number.1_inverter_battery_charge_limit
+        data:
+          value: 95
+```
+
+### Time-of-use mode during peak hours
+```yaml
+automation:
+  - alias: "Solar — TOU Mode During Peak Hours"
+    trigger:
+      - platform: time
+        at: "17:00:00"
+    action:
+      - service: select.select_option
+        target:
+          entity_id: select.1_inverter_operating_mode
+        data:
+          option: "Time-of-Use"
+  - alias: "Solar — Self-Use Mode After Peak"
+    trigger:
+      - platform: time
+        at: "21:00:00"
+    action:
+      - service: select.select_option
+        target:
+          entity_id: select.1_inverter_operating_mode
+        data:
+          option: "Self-Use"
+```
+
+---
+
+## Dashboard Card Examples
+
+### Entities card — quick status overview
+```yaml
+type: entities
+title: ☀️ Solar System
+entities:
+  - entity: sensor.1_inverter_pv_input_power
+    name: Solar Generation
+    icon: mdi:solar-power
+  - entity: sensor.1_inverter_battery_state_of_charge
+    name: Battery Level
+    icon: mdi:battery
+  - entity: sensor.1_inverter_load_power
+    name: Home Load
+    icon: mdi:home-lightning-bolt
+  - entity: sensor.1_inverter_grid_import_power
+    name: Grid Import
+    icon: mdi:transmission-tower-import
+  - entity: sensor.1_inverter_grid_feed_in_power
+    name: Grid Feed-In
+    icon: mdi:transmission-tower-export
+```
+
+### Glance card — at-a-glance numbers
+```yaml
+type: glance
+title: Solar At a Glance
+entities:
+  - entity: sensor.1_inverter_pv_input_power
+    name: PV
+  - entity: sensor.1_inverter_battery_state_of_charge
+    name: Battery
+  - entity: sensor.1_inverter_load_power
+    name: Load
+  - entity: sensor.1_inverter_grid_import_power
+    name: Grid In
+  - entity: sensor.1_inverter_grid_feed_in_power
+    name: Grid Out
+```
+
+### Control panel card
+```yaml
+type: entities
+title: 🎛️ Solar Controls
+entities:
+  - entity: select.1_inverter_operating_mode
+    name: Operating Mode
+  - entity: select.1_inverter_battery_priority
+    name: Battery Priority
+  - entity: number.1_inverter_battery_charge_limit
+    name: Charge Limit
+  - entity: number.1_inverter_battery_discharge_limit
+    name: Discharge Limit
+  - entity: number.1_inverter_grid_charge_limit
+    name: Grid Charge Limit
+  - type: divider
+  - entity: switch.1_inverter_grid_charging
+    name: Grid Charging
+  - entity: switch.1_inverter_grid_feed_in
+    name: Grid Feed-In
+  - entity: switch.1_inverter_backup_mode
+    name: Backup Mode
+```
+
+---
+
+## Troubleshooting
+
+See the full guide: **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)**
+
+Quick reference for the most common issues:
+
+| Symptom | Most likely cause | Fix |
+|---|---|---|
+| "Cannot Connect" on setup | Expired or wrong IOT-Token | Re-copy fresh token from portal DevTools |
+| No devices discovered | Wrong Station ID | Verify 18-digit stationId in portal Network tab |
+| Entities show "Unavailable" | Token expired or portal unreachable | Check HA logs + re-enter token |
+| Sensors always "Unknown" | API returns null for that field | Normal for unsupported sensors on your model |
+| Controls do nothing | Settings endpoint varies by firmware | Check HA logs for endpoint + HTTP status |
+| Integration not in search | Files not installed correctly | Verify `/config/custom_components/solar_of_things/` exists |
+
+### Enable debug logging
+Add to `configuration.yaml`:
+```yaml
+logger:
+  default: info
+  logs:
+    custom_components.solar_of_things: debug
+```
+Restart HA, reproduce the problem, then check **Settings → System → Logs**.
+
+---
+
+## API Reference
+
+This integration targets the same endpoints used by the Siseli web portal.
+
+| Endpoint | Method | Purpose |
+|---|---|---|
+| `/apis/device/list` | POST | Discover all device IDs under a station |
+| `/apis/deviceState/simple/attribute/keys/history/v1` | POST | Real-time time-series telemetry per device |
+| `/apis/stationOverView/stateAttributeSummary/category/yearly` | POST | Monthly energy summary per station |
+
+### Required request headers
+
+| Header | Value |
+|---|---|
+| `IOT-Token` | Your authentication token |
+| `IOT-Time-Zone` | Your timezone string (e.g. `Asia/Manila`) |
+| `Content-Type` | `application/json; charset=utf-8` |
+| `Origin` | `https://solar.siseli.com` |
+
+> Reference client (Node.js CLI with same API behavior):
+> [Hyllesen/solar-of-things-solar-usage](https://github.com/Hyllesen/solar-of-things-solar-usage)
+
+---
+
+## Contributing
+
+Contributions are very welcome!
+
+1. Fork the repository.
+2. Create a feature branch: `git checkout -b feature/my-improvement`
+3. Make your changes and add tests if applicable.
+4. Run the validation workflow locally:
+   ```bash
+   python3 -m py_compile __init__.py api.py config_flow.py \
+     const.py sensor.py number.py select.py switch.py
+   ```
+5. Commit with a clear message and open a Pull Request.
+
+Bug reports and feature requests → use the **GitHub Issues** templates:
+[github.com/conexocasa/solar-of-things-ha/issues/new/choose](https://github.com/conexocasa/solar-of-things-ha/issues/new/choose)
+
+---
+
+## Changelog
+
+See **[CHANGELOG.md](CHANGELOG.md)** for a full history.
+
+| Version | Highlights |
+|---|---|
+| **v2.1.1** | HACS metadata, GitHub Actions CI/CD, issue templates, release script |
+| **v2.1.0** | Station-based device auto-discovery (`/apis/device/list`) |
+| **v2.0.0** | Control entities (battery limits, operating modes, grid switches) |
+| **v1.0.0** | Initial release — monitoring sensors, config flow, multi-station |
+
+---
+
+## License
+
+This project is licensed under the **MIT License** — see [LICENSE](LICENSE) for details.
+
+---
+
+## Disclaimer
+
+This is an **unofficial** community integration and is not affiliated with,
+endorsed by, or supported by Siseli or Solar of Things.
+Use at your own risk. The API may change without notice.
+
+---
+
+<p align="center">
+  Made with ☀️ for the Home Assistant community<br>
+  <a href="https://github.com/conexocasa/solar-of-things-ha/issues">Report a Bug</a> ·
+  <a href="https://github.com/conexocasa/solar-of-things-ha/issues">Request a Feature</a> ·
+  <a href="https://community.home-assistant.io">HA Community Forum</a>
+</p>

+ 148 - 0
custom_components/solar_of_things/__init__.py

@@ -0,0 +1,148 @@
+"""The Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+from datetime import timedelta
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .api import SolarOfThingsAPI
+from .const import DOMAIN, CONF_DEVICE_ID
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.NUMBER, Platform.SELECT, Platform.SWITCH]
+
+DEVICE_UPDATE_INTERVAL = timedelta(minutes=5)
+STATION_UPDATE_INTERVAL = timedelta(minutes=30)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Solar of Things from a config entry."""
+    hass.data.setdefault(DOMAIN, {})
+
+    api = SolarOfThingsAPI(
+        iot_token=entry.data["iot_token"],
+        time_zone=entry.data.get("time_zone") or entry.options.get("time_zone"),
+    )
+
+    station_id = entry.data["station_id"]
+    configured_device_id = (entry.data.get(CONF_DEVICE_ID) or "").strip()
+
+    # Station coordinator (device list + monthly)
+    station_coordinator = SolarOfThingsStationCoordinator(
+        hass=hass,
+        api=api,
+        station_id=station_id,
+    )
+    await station_coordinator.async_config_entry_first_refresh()
+
+    # Create one coordinator per device
+    devices = station_coordinator.data.get("devices", []) if station_coordinator.data else []
+
+    # If the user configured a specific deviceId, only create entities for that device.
+    # (We still keep station-level data like monthly summary.)
+    if configured_device_id:
+        filtered = [d for d in devices if str(d.get("id")) == configured_device_id]
+        if filtered:
+            devices = filtered
+        else:
+            # Fallback: allow the integration to work even if the device isn't returned
+            # by the station device list (permissions / pagination / API quirks).
+            devices = [{"id": configured_device_id, "name": configured_device_id}]
+    device_coordinators: dict[str, SolarOfThingsDeviceCoordinator] = {}
+
+    for dev in devices:
+        device_id = str(dev.get("id") or "")
+        if not device_id:
+            continue
+
+        c = SolarOfThingsDeviceCoordinator(
+            hass=hass,
+            api=api,
+            station_id=station_id,
+            device=device_id,
+            device_meta=dev,
+        )
+        await c.async_config_entry_first_refresh()
+        device_coordinators[device_id] = c
+
+    hass.data[DOMAIN][entry.entry_id] = {
+        "api": api,
+        "station_id": station_id,
+        "station_coordinator": station_coordinator,
+        "device_coordinators": device_coordinators,
+        "devices": devices,
+    }
+
+    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload a config entry."""
+    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+        hass.data[DOMAIN].pop(entry.entry_id, None)
+    return unload_ok
+
+
+class SolarOfThingsStationCoordinator(DataUpdateCoordinator):
+    """Fetch station-level data (device list + monthly stats)."""
+
+    def __init__(self, hass: HomeAssistant, api: SolarOfThingsAPI, station_id: str) -> None:
+        self.api = api
+        self.station_id = station_id
+        super().__init__(
+            hass,
+            _LOGGER,
+            name=f"{DOMAIN}_station_{station_id}",
+            update_interval=STATION_UPDATE_INTERVAL,
+        )
+
+    async def _async_update_data(self):
+        try:
+            devices = await self.hass.async_add_executor_job(self.api.list_devices, self.station_id)
+            monthly = await self.hass.async_add_executor_job(self.api.fetch_monthly_summary, self.station_id)
+            return {"devices": devices, "monthly": monthly}
+        except Exception as err:
+            raise UpdateFailed(f"Station update failed: {err}")
+
+
+class SolarOfThingsDeviceCoordinator(DataUpdateCoordinator):
+    """Fetch device-level telemetry + settings."""
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        api: SolarOfThingsAPI,
+        station_id: str,
+        device: str,
+        device_meta: dict,
+    ) -> None:
+        self.api = api
+        self.station_id = station_id
+        self.device_id = device
+        self.device_meta = device_meta
+        super().__init__(
+            hass,
+            _LOGGER,
+            name=f"{DOMAIN}_device_{device}",
+            update_interval=DEVICE_UPDATE_INTERVAL,
+        )
+
+    async def _async_update_data(self):
+        try:
+            time_series = await self.hass.async_add_executor_job(self.api.fetch_latest_data, self.device_id)
+            settings = await self.hass.async_add_executor_job(self.api.fetch_settings, self.device_id)
+            return {
+                "time_series": time_series,
+                "settings": settings,
+                "device": self.device_id,
+                "station_id": self.station_id,
+                "device_meta": self.device_meta,
+            }
+        except Exception as err:
+            raise UpdateFailed(f"Device update failed for {self.device_id}: {err}")

+ 325 - 0
custom_components/solar_of_things/api.py

@@ -0,0 +1,325 @@
+"""API client for Solar of Things (solar.siseli.com)."""
+from __future__ import annotations
+
+import logging
+from datetime import datetime, timedelta
+from typing import Any
+
+import requests
+
+try:
+    # Python 3.9+
+    from zoneinfo import ZoneInfo
+except Exception:  # pragma: no cover
+    ZoneInfo = None
+
+from .const import (
+    API_BASE_URL,
+    API_TIME_SERIES,
+    API_MONTHLY_SUMMARY,
+    API_DEVICE_LIST,
+    API_SETTINGS_GET,
+    API_SETTINGS_SET,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+_DEFAULT_TZ = "Asia/Manila"
+
+
+class SolarOfThingsAPI:
+    """Solar of Things API wrapper.
+
+    Design note:
+    - token + timezone live on this object
+    - stationId/deviceId are passed per call (so one integration can handle many devices)
+    """
+
+    def __init__(self, iot_token: str, time_zone: str | None = None) -> None:
+        self.iot_token = iot_token
+        self.time_zone = time_zone or _DEFAULT_TZ
+
+        self.session = requests.Session()
+        self.session.headers.update(
+            {
+                "Accept": "application/json",
+                "Content-Type": "application/json; charset=utf-8",
+                "IOT-Token": self.iot_token,
+                "IOT-Time-Zone": self.time_zone,
+                "Origin": "https://solar.siseli.com",
+                "Referer": "https://solar.siseli.com/",
+                "User-Agent": "HomeAssistant-SolarOfThings/2.1.0 (+https://www.home-assistant.io)",
+            }
+        )
+
+    def _now(self) -> datetime:
+        if ZoneInfo:
+            try:
+                return datetime.now(tz=ZoneInfo(self.time_zone))
+            except Exception:
+                return datetime.now()
+        return datetime.now()
+
+    def _format_time(self, dt: datetime) -> str:
+        """Format timestamp like the Siseli web client expects."""
+        if ZoneInfo:
+            try:
+                dt = dt.astimezone(ZoneInfo(self.time_zone))
+            except Exception:
+                pass
+        return dt.replace(microsecond=0).isoformat()
+
+    # -------------------------
+    # Station -> device listing
+    # -------------------------
+    def list_devices(self, station_id: str, page_size: int = 50) -> list[dict[str, Any]]:
+        """Return all devices under a station.
+
+        Uses endpoint discovered from the portal JS bundle: /apis/device/list.
+        Expected payload: {page, count, stationId}
+        """
+        devices: list[dict[str, Any]] = []
+
+        page = 1
+        total = None
+
+        while True:
+            payload = {"page": page, "count": page_size, "stationId": station_id}
+            resp = self.session.post(f"{API_BASE_URL}{API_DEVICE_LIST}", json=payload, timeout=30)
+            resp.raise_for_status()
+            data = resp.json()
+
+            if data.get("code") not in (0, None):
+                raise RuntimeError(f"Device list error code={data.get('code')} message={data.get('message')}")
+
+            d = data.get("data") or {}
+            total = d.get("total", total)
+            batch = d.get("list") or []
+            if not isinstance(batch, list):
+                batch = []
+
+            devices.extend(batch)
+
+            if total is None:
+                # no pagination info; stop when returned less than requested
+                if len(batch) < page_size:
+                    break
+            else:
+                if len(devices) >= int(total):
+                    break
+
+            if not batch:
+                break
+
+            page += 1
+
+        return devices
+
+    # -------------------------
+    # Time-series (per device)
+    # -------------------------
+    def fetch_latest_data(self, device_id: str) -> dict[str, Any]:
+        """Fetch the latest readings for a device."""
+        end_time = self._now()
+        start_time = end_time - timedelta(hours=1)
+
+        keys = [
+            "pvInputPower",
+            "acOutputActivePower",
+            "batteryDischargeCurrent",
+            "batteryChargingCurrent",
+            "batteryVoltage",
+            "feedInPower",
+            "batterySOC",
+        ]
+
+        payload = {
+            "deviceId": device_id,
+            "count": 2000,
+            "page": 1,
+            "fromTime": self._format_time(start_time),
+            "toTime": self._format_time(end_time),
+            "orderByTimeAsc": True,
+            "keys": keys,
+        }
+
+        resp = self.session.post(f"{API_BASE_URL}{API_TIME_SERIES}", json=payload, timeout=30)
+        resp.raise_for_status()
+        data = resp.json()
+
+        if data.get("code") not in (0, None):
+            raise RuntimeError(f"Timeseries error code={data.get('code')} message={data.get('message')}")
+
+        payload_data = (data.get("data") or {}).get("payload") or {}
+        fields = payload_data.get("fields") or {}
+
+        latest_values: dict[str, Any] = {}
+        for key, arr in fields.items():
+            if isinstance(arr, list) and arr:
+                latest_values[key] = arr[-1]
+
+        # Unit normalization:
+        # - pvInputPower appears to be W
+        # - acOutputActivePower is kW in API; normalize to W
+        if "acOutputActivePower" in latest_values:
+            try:
+                latest_values["acOutputActivePower"] = float(latest_values["acOutputActivePower"]) * 1000.0
+            except Exception:
+                pass
+
+        # Derived values
+        voltage = float(latest_values.get("batteryVoltage") or 0)
+        discharge = float(latest_values.get("batteryDischargeCurrent") or 0)
+        charge = float(latest_values.get("batteryChargingCurrent") or 0)
+        latest_values["batteryPower"] = (discharge - charge) * voltage
+
+        pv_power = float(latest_values.get("pvInputPower") or 0)
+        ac_output = float(latest_values.get("acOutputActivePower") or 0)
+        feed_in = float(latest_values.get("feedInPower") or 0)
+        battery_power = float(latest_values.get("batteryPower") or 0)
+
+        latest_values["gridPower"] = max(0.0, ac_output - pv_power + battery_power + feed_in)
+        latest_values["loadPower"] = ac_output
+
+        return latest_values
+
+    # -------------------------
+    # Monthly summary (station)
+    # -------------------------
+    def fetch_monthly_summary(self, station_id: str) -> dict[str, Any]:
+        """Fetch monthly PV summary for current month."""
+        now = self._now()
+        year = now.year
+        month_key = f"{year}-{str(now.month).zfill(2)}"
+
+        url = (
+            f"{API_BASE_URL}{API_MONTHLY_SUMMARY}"
+            f"?stationId={station_id}&summaryCategoryKey=pvInverterElectricityQuantityClass"
+        )
+
+        resp = self.session.post(url, json={"time": str(year)}, timeout=30)
+        resp.raise_for_status()
+        data = resp.json()
+
+        if data.get("code") not in (0, None):
+            raise RuntimeError(f"Monthly summary error code={data.get('code')} message={data.get('message')}")
+
+        props = (((data.get("data") or {}).get("properties")) or [])
+        out: dict[str, Any] = {}
+
+        for prop in props:
+            prop_key = ((prop.get("property") or {}).get("key"))
+            if not prop_key:
+                continue
+            for tp in prop.get("timePoints") or []:
+                if tp.get("time") == month_key and tp.get("isRealValue"):
+                    out[prop_key] = tp.get("value")
+
+        if "pvGeneratedEnergy" in out:
+            return {"monthly_pv_generated": float(out.get("pvGeneratedEnergy") or 0)}
+
+        return {}
+
+    # -------------------------
+    # Settings / control (per device)
+    # -------------------------
+    def fetch_settings(self, device_id: str) -> dict[str, Any]:
+        """Fetch current device settings.
+
+        NOTE: These endpoints may vary by account/device firmware. If your portal uses
+        different endpoints, update const.py accordingly.
+        """
+        payload = {"deviceId": device_id}
+
+        try:
+            response = self.session.post(
+                f"{API_BASE_URL}{API_SETTINGS_GET}",
+                json=payload,
+                timeout=30,
+            )
+            response.raise_for_status()
+            data = response.json()
+
+            settings: dict[str, Any] = {}
+            if "data" in data:
+                sd = data["data"]
+                settings["batteryChargeLimit"] = sd.get("batteryChargeLimit", 100)
+                settings["batteryDischargeLimit"] = sd.get("batteryDischargeLimit", 10)
+                settings["gridChargeLimit"] = sd.get("gridChargeLimit", 0)
+                settings["operatingMode"] = sd.get("operatingMode", "Self-Use")
+                settings["batteryPriority"] = sd.get("batteryPriority", "Solar First")
+                settings["gridChargingEnabled"] = sd.get("gridChargingEnabled", False)
+                settings["gridFeedInEnabled"] = sd.get("gridFeedInEnabled", True)
+                settings["backupModeEnabled"] = sd.get("backupModeEnabled", False)
+
+            return settings
+
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error fetching settings for device %s: %s", device_id, err)
+            return {}
+
+    def _update_setting(self, device_id: str, setting_key: str, value: Any) -> bool:
+        payload = {"deviceId": device_id, "settings": {setting_key: value}}
+
+        try:
+            response = self.session.post(
+                f"{API_BASE_URL}{API_SETTINGS_SET}",
+                json=payload,
+                timeout=30,
+            )
+            response.raise_for_status()
+            data = response.json()
+
+            if data.get("success") is True or data.get("code") in (0, None):
+                _LOGGER.info("Updated %s=%s for device %s", setting_key, value, device_id)
+                return True
+
+            _LOGGER.error("Failed to update %s for device %s: %s", setting_key, device_id, data)
+            return False
+
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error updating %s for device %s: %s", setting_key, device_id, err)
+            return False
+
+    def set_battery_charge_limit(self, device_id: str, limit: int) -> bool:
+        return self._update_setting(device_id, "batteryChargeLimit", limit)
+
+    def set_battery_discharge_limit(self, device_id: str, limit: int) -> bool:
+        return self._update_setting(device_id, "batteryDischargeLimit", limit)
+
+    def set_grid_charge_limit(self, device_id: str, limit: int) -> bool:
+        return self._update_setting(device_id, "gridChargeLimit", limit)
+
+    def set_operating_mode(self, device_id: str, mode: str) -> bool:
+        return self._update_setting(device_id, "operatingMode", mode)
+
+    def set_battery_priority(self, device_id: str, priority: str) -> bool:
+        return self._update_setting(device_id, "batteryPriority", priority)
+
+    def set_grid_charging(self, device_id: str, enabled: bool) -> bool:
+        return self._update_setting(device_id, "gridChargingEnabled", enabled)
+
+    def set_grid_feed_in(self, device_id: str, enabled: bool) -> bool:
+        return self._update_setting(device_id, "gridFeedInEnabled", enabled)
+
+    def set_backup_mode(self, device_id: str, enabled: bool) -> bool:
+        return self._update_setting(device_id, "backupModeEnabled", enabled)
+
+    # -------------------------
+    # Validation
+    # -------------------------
+    def test_connection(self, station_id: str) -> bool:
+        """Validate token + station by listing devices and calling timeseries on first."""
+        try:
+            devices = self.list_devices(station_id)
+            if not devices:
+                return False
+            first = devices[0]
+            device_id = str(first.get("id") or "")
+            if not device_id:
+                return False
+            _ = self.fetch_latest_data(device_id)
+            return True
+        except Exception as err:
+            _LOGGER.error("Connection test failed: %s", err)
+            return False

+ 94 - 0
custom_components/solar_of_things/config_flow.py

@@ -0,0 +1,94 @@
+"""Config flow for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
+
+from .api import SolarOfThingsAPI
+from .const import (
+    DOMAIN,
+    CONF_IOT_TOKEN,
+    CONF_STATION_ID,
+    CONF_DEVICE_ID,
+    CONF_TIME_ZONE,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
+    """Validate the user input allows us to connect."""
+
+    api = SolarOfThingsAPI(iot_token=data[CONF_IOT_TOKEN], time_zone=data.get(CONF_TIME_ZONE))
+
+    station_id = data[CONF_STATION_ID]
+    device_id = (data.get(CONF_DEVICE_ID) or "").strip()
+
+    try:
+        # If the user provides a deviceId, validate it directly.
+        # Otherwise validate the station by listing devices and querying the first device.
+        if device_id:
+            res = await hass.async_add_executor_job(api.fetch_latest_data, device_id)
+            ok = bool(res)
+        else:
+            ok = await hass.async_add_executor_job(api.test_connection, station_id)
+    except Exception as err:
+        _LOGGER.error("Validation failed: %s", err)
+        ok = False
+    if not ok:
+        raise CannotConnect
+
+    return {"title": f"Solar Station {station_id}"}
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Solar of Things."""
+
+    VERSION = 2
+
+    async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult:
+        errors: dict[str, str] = {}
+
+        if user_input is not None:
+            try:
+                info = await validate_input(self.hass, user_input)
+            except CannotConnect:
+                errors["base"] = "cannot_connect"
+            except Exception:  # pylint: disable=broad-except
+                _LOGGER.exception("Unexpected exception")
+                errors["base"] = "unknown"
+            else:
+                await self.async_set_unique_id(f"station_{user_input[CONF_STATION_ID]}")
+                self._abort_if_unique_id_configured()
+                return self.async_create_entry(title=info["title"], data=user_input)
+
+        data_schema = vol.Schema(
+            {
+                vol.Required(CONF_IOT_TOKEN): cv.string,
+                vol.Required(CONF_STATION_ID): cv.string,
+                # Optional: user can force a single device under the station.
+                vol.Optional(CONF_DEVICE_ID): cv.string,
+                vol.Optional(CONF_TIME_ZONE, default="Asia/Manila"): cv.string,
+            }
+        )
+
+        return self.async_show_form(
+            step_id="user",
+            data_schema=data_schema,
+            errors=errors,
+            description_placeholders={
+                "docs_url": "https://github.com/Hyllesen/solar-of-things-solar-usage"
+            },
+        )
+
+
+class CannotConnect(HomeAssistantError):
+    """Error to indicate we cannot connect."""

+ 123 - 0
custom_components/solar_of_things/const.py

@@ -0,0 +1,123 @@
+"""Constants for the Solar of Things integration."""
+
+DOMAIN = "solar_of_things"
+
+# Configuration keys
+CONF_IOT_TOKEN = "iot_token"
+CONF_STATION_ID = "station_id"
+CONF_DEVICE_ID = "device_id"
+CONF_TIME_ZONE = "time_zone"
+
+# API endpoints
+API_BASE_URL = "https://solar.siseli.com"
+# Endpoints based on the working reference client
+# https://github.com/Hyllesen/solar-of-things-solar-usage
+API_TIME_SERIES = "/apis/deviceState/simple/attribute/keys/history/v1"
+API_MONTHLY_SUMMARY = "/apis/stationOverView/stateAttributeSummary/category/yearly"
+API_SETTINGS_GET = "/api/device/settings/v1"
+API_SETTINGS_SET = "/api/device/settings/update/v1"
+
+# Station -> devices
+API_DEVICE_LIST = "/apis/device/list"
+
+# Sensor keys
+SENSOR_KEYS = [
+    "pvInputPower",
+    "acOutputActivePower",
+    "batteryDischargeCurrent",
+    "batteryChargingCurrent",
+    "batteryVoltage",
+    "feedInPower",
+    "batteryPower",
+    "batterySOC",
+    "gridPower",
+    "loadPower",
+]
+
+# Sensor names and units
+SENSOR_DEFINITIONS = {
+    "pvInputPower": {
+        "name": "PV Input Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:solar-power",
+    },
+    "acOutputActivePower": {
+        "name": "AC Output Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:power-plug",
+    },
+    "batteryDischargeCurrent": {
+        "name": "Battery Discharge Current",
+        "unit": "A",
+        "device_class": "current",
+        "icon": "mdi:battery-arrow-down",
+    },
+    "batteryChargingCurrent": {
+        "name": "Battery Charging Current",
+        "unit": "A",
+        "device_class": "current",
+        "icon": "mdi:battery-arrow-up",
+    },
+    "batteryVoltage": {
+        "name": "Battery Voltage",
+        "unit": "V",
+        "device_class": "voltage",
+        "icon": "mdi:battery",
+    },
+    "batteryPower": {
+        "name": "Battery Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:battery-charging",
+    },
+    "batterySOC": {
+        "name": "Battery State of Charge",
+        "unit": "%",
+        "device_class": "battery",
+        "icon": "mdi:battery",
+    },
+    "feedInPower": {
+        "name": "Grid Feed-in Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:transmission-tower-export",
+    },
+    "gridPower": {
+        "name": "Grid Import Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:transmission-tower-import",
+    },
+    "loadPower": {
+        "name": "Load Power",
+        "unit": "W",
+        "device_class": "power",
+        "icon": "mdi:home-lightning-bolt",
+    },
+    # Monthly summary sensors
+    "monthly_pv_generated": {
+        "name": "Monthly PV Generated",
+        "unit": "kWh",
+        "device_class": "energy",
+        "icon": "mdi:solar-power",
+    },
+    "monthly_grid_import": {
+        "name": "Monthly Grid Import",
+        "unit": "kWh",
+        "device_class": "energy",
+        "icon": "mdi:transmission-tower-import",
+    },
+    "monthly_total_consumption": {
+        "name": "Monthly Total Consumption",
+        "unit": "kWh",
+        "device_class": "energy",
+        "icon": "mdi:home-lightning-bolt",
+    },
+    "monthly_solar_percentage": {
+        "name": "Monthly Solar Coverage",
+        "unit": "%",
+        "icon": "mdi:percent",
+    },
+}

+ 11 - 0
custom_components/solar_of_things/manifest.json

@@ -0,0 +1,11 @@
+{
+  "domain": "solar_of_things",
+  "name": "Solar of Things",
+  "codeowners": [],
+  "config_flow": true,
+  "documentation": "https://github.com/conexocasa/solar-of-things-ha",
+  "iot_class": "cloud_polling",
+  "issue_tracker": "https://github.com/conexocasa/solar-of-things-ha/issues",
+  "requirements": [],
+  "version": "2.1.1"
+}

+ 131 - 0
custom_components/solar_of_things/number.py

@@ -0,0 +1,131 @@
+"""Number platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+
+from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import PERCENTAGE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up number entities (controls) for each device."""
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    api = data["api"]
+    station_id: str = data["station_id"]
+    device_coordinators = data["device_coordinators"]
+
+    entities: list[NumberEntity] = []
+
+    for device_id, coordinator in device_coordinators.items():
+        device_name = (coordinator.device_meta or {}).get("name") or device_id
+        entities.extend(
+            [
+                SolarOfThingsBatteryChargeLimitNumber(api, coordinator, station_id, device_id, device_name),
+                SolarOfThingsBatteryDischargeLimitNumber(api, coordinator, station_id, device_id, device_name),
+                SolarOfThingsGridChargeLimitNumber(api, coordinator, station_id, device_id, device_name),
+            ]
+        )
+
+    async_add_entities(entities)
+
+
+class _BaseNumber(CoordinatorEntity, NumberEntity):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(coordinator)
+        self._api = api
+        self._station_id = station_id
+        self._device_id = device_id
+        self._device_name = device_name
+
+    @property
+    def device_info(self):
+        return {
+            "identifiers": {(DOMAIN, self._station_id, self._device_id)},
+            "name": self._device_name,
+            "manufacturer": "Siseli",
+            "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
+            "via_device": (DOMAIN, self._station_id),
+        }
+
+
+class SolarOfThingsBatteryChargeLimitNumber(_BaseNumber):
+    _setting_key = "batteryChargeLimit"
+
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Battery Charge Limit"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_charge_limit"
+        self._attr_native_min_value = 0
+        self._attr_native_max_value = 100
+        self._attr_native_step = 1
+        self._attr_native_unit_of_measurement = PERCENTAGE
+        self._attr_mode = NumberMode.SLIDER
+        self._attr_icon = "mdi:battery-arrow-up"
+
+    @property
+    def native_value(self):
+        return ((self.coordinator.data or {}).get("settings") or {}).get(self._setting_key)
+
+    async def async_set_native_value(self, value: float) -> None:
+        await self.hass.async_add_executor_job(self._api.set_battery_charge_limit, self._device_id, int(value))
+        await self.coordinator.async_request_refresh()
+
+
+class SolarOfThingsBatteryDischargeLimitNumber(_BaseNumber):
+    _setting_key = "batteryDischargeLimit"
+
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Battery Discharge Limit"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_discharge_limit"
+        self._attr_native_min_value = 0
+        self._attr_native_max_value = 100
+        self._attr_native_step = 1
+        self._attr_native_unit_of_measurement = PERCENTAGE
+        self._attr_mode = NumberMode.SLIDER
+        self._attr_icon = "mdi:battery-arrow-down"
+
+    @property
+    def native_value(self):
+        return ((self.coordinator.data or {}).get("settings") or {}).get(self._setting_key)
+
+    async def async_set_native_value(self, value: float) -> None:
+        await self.hass.async_add_executor_job(self._api.set_battery_discharge_limit, self._device_id, int(value))
+        await self.coordinator.async_request_refresh()
+
+
+class SolarOfThingsGridChargeLimitNumber(_BaseNumber):
+    _setting_key = "gridChargeLimit"
+
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Grid Charge Limit"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_charge_limit"
+        self._attr_native_min_value = 0
+        self._attr_native_max_value = 5000
+        self._attr_native_step = 100
+        self._attr_native_unit_of_measurement = "W"
+        self._attr_mode = NumberMode.SLIDER
+        self._attr_device_class = NumberDeviceClass.POWER
+        self._attr_icon = "mdi:transmission-tower-import"
+
+    @property
+    def native_value(self):
+        return ((self.coordinator.data or {}).get("settings") or {}).get(self._setting_key)
+
+    async def async_set_native_value(self, value: float) -> None:
+        await self.hass.async_add_executor_job(self._api.set_grid_charge_limit, self._device_id, int(value))
+        await self.coordinator.async_request_refresh()

+ 90 - 0
custom_components/solar_of_things/select.py

@@ -0,0 +1,90 @@
+"""Select platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+
+from homeassistant.components.select import SelectEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+OPERATING_MODES = ["Self-Use", "Time-of-Use", "Backup", "Grid-Tie", "Off-Grid"]
+BATTERY_PRIORITY_MODES = ["Solar First", "Battery First", "Grid First"]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
+    data = hass.data[DOMAIN][entry.entry_id]
+    api = data["api"]
+    station_id: str = data["station_id"]
+    device_coordinators = data["device_coordinators"]
+
+    entities: list[SelectEntity] = []
+
+    for device_id, coordinator in device_coordinators.items():
+        device_name = (coordinator.device_meta or {}).get("name") or device_id
+        entities.extend(
+            [
+                SolarOfThingsOperatingModeSelect(api, coordinator, station_id, device_id, device_name),
+                SolarOfThingsBatteryPrioritySelect(api, coordinator, station_id, device_id, device_name),
+            ]
+        )
+
+    async_add_entities(entities)
+
+
+class _BaseSelect(CoordinatorEntity, SelectEntity):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(coordinator)
+        self._api = api
+        self._station_id = station_id
+        self._device_id = device_id
+        self._device_name = device_name
+
+    @property
+    def device_info(self):
+        return {
+            "identifiers": {(DOMAIN, self._station_id, self._device_id)},
+            "name": self._device_name,
+            "manufacturer": "Siseli",
+            "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
+            "via_device": (DOMAIN, self._station_id),
+        }
+
+
+class SolarOfThingsOperatingModeSelect(_BaseSelect):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Operating Mode"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_operating_mode"
+        self._attr_options = OPERATING_MODES
+        self._attr_icon = "mdi:cog"
+
+    @property
+    def current_option(self):
+        return ((self.coordinator.data or {}).get("settings") or {}).get("operatingMode")
+
+    async def async_select_option(self, option: str) -> None:
+        await self.hass.async_add_executor_job(self._api.set_operating_mode, self._device_id, option)
+        await self.coordinator.async_request_refresh()
+
+
+class SolarOfThingsBatteryPrioritySelect(_BaseSelect):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Battery Priority"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_priority"
+        self._attr_options = BATTERY_PRIORITY_MODES
+        self._attr_icon = "mdi:battery-sync"
+
+    @property
+    def current_option(self):
+        return ((self.coordinator.data or {}).get("settings") or {}).get("batteryPriority")
+
+    async def async_select_option(self, option: str) -> None:
+        await self.hass.async_add_executor_job(self._api.set_battery_priority, self._device_id, option)
+        await self.coordinator.async_request_refresh()

+ 191 - 0
custom_components/solar_of_things/sensor.py

@@ -0,0 +1,191 @@
+"""Sensor platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+
+from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    PERCENTAGE,
+    UnitOfElectricCurrent,
+    UnitOfElectricPotential,
+    UnitOfEnergy,
+    UnitOfPower,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN, SENSOR_DEFINITIONS
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up Solar of Things sensors."""
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    station_id: str = data["station_id"]
+    device_coordinators = data["device_coordinators"]
+    station_coordinator = data["station_coordinator"]
+
+    entities: list[SensorEntity] = []
+
+    # Per-device sensors
+    for device_id, coordinator in device_coordinators.items():
+        device_name = (coordinator.device_meta or {}).get("name") or device_id
+
+        for key, definition in SENSOR_DEFINITIONS.items():
+            if key.startswith("monthly_"):
+                continue
+
+            entities.append(
+                SolarOfThingsDeviceSensor(
+                    coordinator=coordinator,
+                    station_id=station_id,
+                    device_id=device_id,
+                    device_name=device_name,
+                    sensor_key=key,
+                    sensor_definition=definition,
+                )
+            )
+
+    # Station-level monthly sensors
+    if station_coordinator:
+        for key, definition in SENSOR_DEFINITIONS.items():
+            if not key.startswith("monthly_"):
+                continue
+
+            entities.append(
+                SolarOfThingsStationMonthlySensor(
+                    coordinator=station_coordinator,
+                    station_id=station_id,
+                    sensor_key=key,
+                    sensor_definition=definition,
+                )
+            )
+
+    async_add_entities(entities)
+
+
+class SolarOfThingsDeviceSensor(CoordinatorEntity, SensorEntity):
+    """Per-device telemetry sensor."""
+
+    def __init__(
+        self,
+        coordinator,
+        station_id: str,
+        device_id: str,
+        device_name: str,
+        sensor_key: str,
+        sensor_definition: dict,
+    ) -> None:
+        super().__init__(coordinator)
+
+        self._station_id = station_id
+        self._device_id = device_id
+        self._device_name = device_name
+        self._sensor_key = sensor_key
+        self._sensor_definition = sensor_definition
+
+        self._attr_name = f"{device_name} {sensor_definition['name']}"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_{sensor_key}"
+        self._attr_icon = sensor_definition.get("icon")
+
+        unit = sensor_definition.get("unit")
+        if unit == "W":
+            self._attr_device_class = SensorDeviceClass.POWER
+            self._attr_native_unit_of_measurement = UnitOfPower.WATT
+            self._attr_state_class = SensorStateClass.MEASUREMENT
+        elif unit == "kWh":
+            self._attr_device_class = SensorDeviceClass.ENERGY
+            self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
+            self._attr_state_class = SensorStateClass.TOTAL_INCREASING
+        elif unit == "A":
+            self._attr_device_class = SensorDeviceClass.CURRENT
+            self._attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
+            self._attr_state_class = SensorStateClass.MEASUREMENT
+        elif unit == "V":
+            self._attr_device_class = SensorDeviceClass.VOLTAGE
+            self._attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
+            self._attr_state_class = SensorStateClass.MEASUREMENT
+        elif unit == "%":
+            if "battery" in sensor_key.lower():
+                self._attr_device_class = SensorDeviceClass.BATTERY
+            self._attr_native_unit_of_measurement = PERCENTAGE
+            self._attr_state_class = SensorStateClass.MEASUREMENT
+
+    @property
+    def device_info(self):
+        return {
+            "identifiers": {(DOMAIN, self._station_id, self._device_id)},
+            "name": self._device_name,
+            "manufacturer": "Siseli",
+            "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
+            "via_device": (DOMAIN, self._station_id),
+        }
+
+    @property
+    def native_value(self):
+        ts = (self.coordinator.data or {}).get("time_series") or {}
+        val = ts.get(self._sensor_key)
+        if val is None:
+            return None
+        try:
+            return round(float(val), 2)
+        except Exception:
+            return None
+
+
+class SolarOfThingsStationMonthlySensor(CoordinatorEntity, SensorEntity):
+    """Station-level monthly summary sensor."""
+
+    def __init__(
+        self,
+        coordinator,
+        station_id: str,
+        sensor_key: str,
+        sensor_definition: dict,
+    ) -> None:
+        super().__init__(coordinator)
+
+        self._station_id = station_id
+        self._sensor_key = sensor_key
+        self._sensor_definition = sensor_definition
+
+        self._attr_name = f"Station {station_id} {sensor_definition['name']}"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{sensor_key}"
+        self._attr_icon = sensor_definition.get("icon")
+
+        unit = sensor_definition.get("unit")
+        if unit == "kWh":
+            self._attr_device_class = SensorDeviceClass.ENERGY
+            self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
+            self._attr_state_class = SensorStateClass.TOTAL
+        elif unit == "%":
+            self._attr_native_unit_of_measurement = PERCENTAGE
+            self._attr_state_class = SensorStateClass.MEASUREMENT
+
+    @property
+    def device_info(self):
+        return {
+            "identifiers": {(DOMAIN, self._station_id)},
+            "name": f"Solar Station {self._station_id}",
+            "manufacturer": "Siseli",
+            "model": "Station",
+        }
+
+    @property
+    def native_value(self):
+        monthly = (self.coordinator.data or {}).get("monthly") or {}
+        val = monthly.get(self._sensor_key)
+        if val is None:
+            return None
+        try:
+            return round(float(val), 2)
+        except Exception:
+            return None

+ 84 - 0
custom_components/solar_of_things/strings.json

@@ -0,0 +1,84 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "title": "Add Solar of Things Station",
+        "description": "Configure your Solar of Things station. You can add multiple stations by configuring this integration multiple times.\n\nTo find your credentials:\n1. Log into https://solar.siseli.com\n2. Open browser developer tools (F12) → Network tab\n3. Refresh the page and look at API requests\n4. Copy the IOT-Token from request headers\n5. Find stationId and deviceId in the request URLs or payloads\n\nMore info: {docs_url}",
+        "data": {
+          "iot_token": "IOT Token",
+          "station_id": "Station ID (required)",
+          "device_id": "Device ID (optional; leave blank to auto-discover all devices)",
+          "time_zone": "Time zone (IOT-Time-Zone header)"
+        }
+      },
+      "add_another": {
+        "title": "Add Another Station?",
+        "description": "You have added {station_count} station(s). Would you like to add another?",
+        "data": {
+          "add_another": "Add another station"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "Failed to connect to Solar of Things API. Please check your IOT Token and IDs.",
+      "unknown": "Unexpected error occurred. Please check the logs."
+    },
+    "abort": {
+      "already_configured": "This station is already configured."
+    }
+  },
+  "options": {
+    "step": {
+      "init": {
+        "title": "Solar of Things Options",
+        "data": {
+          "update_interval": "Update interval (minutes)"
+        }
+      }
+    }
+  },
+  "entity": {
+    "number": {
+      "battery_charge_limit": {
+        "name": "Battery Charge Limit"
+      },
+      "battery_discharge_limit": {
+        "name": "Battery Discharge Limit"
+      },
+      "grid_charge_limit": {
+        "name": "Grid Charge Limit"
+      }
+    },
+    "select": {
+      "operating_mode": {
+        "name": "Operating Mode",
+        "state": {
+          "self_use": "Self-Use",
+          "time_of_use": "Time-of-Use",
+          "backup": "Backup",
+          "grid_tie": "Grid-Tie",
+          "off_grid": "Off-Grid"
+        }
+      },
+      "battery_priority": {
+        "name": "Battery Priority",
+        "state": {
+          "solar_first": "Solar First",
+          "battery_first": "Battery First",
+          "grid_first": "Grid First"
+        }
+      }
+    },
+    "switch": {
+      "grid_charging": {
+        "name": "Grid Charging"
+      },
+      "grid_feed_in": {
+        "name": "Grid Feed-In"
+      },
+      "backup_mode": {
+        "name": "Backup Mode"
+      }
+    }
+  }
+}

+ 117 - 0
custom_components/solar_of_things/switch.py

@@ -0,0 +1,117 @@
+"""Switch platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+
+from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
+    data = hass.data[DOMAIN][entry.entry_id]
+    api = data["api"]
+    station_id: str = data["station_id"]
+    device_coordinators = data["device_coordinators"]
+
+    entities: list[SwitchEntity] = []
+
+    for device_id, coordinator in device_coordinators.items():
+        device_name = (coordinator.device_meta or {}).get("name") or device_id
+        entities.extend(
+            [
+                SolarOfThingsGridChargingSwitch(api, coordinator, station_id, device_id, device_name),
+                SolarOfThingsGridFeedInSwitch(api, coordinator, station_id, device_id, device_name),
+                SolarOfThingsBackupModeSwitch(api, coordinator, station_id, device_id, device_name),
+            ]
+        )
+
+    async_add_entities(entities)
+
+
+class _BaseSwitch(CoordinatorEntity, SwitchEntity):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(coordinator)
+        self._api = api
+        self._station_id = station_id
+        self._device_id = device_id
+        self._device_name = device_name
+
+    @property
+    def device_info(self):
+        return {
+            "identifiers": {(DOMAIN, self._station_id, self._device_id)},
+            "name": self._device_name,
+            "manufacturer": "Siseli",
+            "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
+            "via_device": (DOMAIN, self._station_id),
+        }
+
+
+class SolarOfThingsGridChargingSwitch(_BaseSwitch):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Grid Charging"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_charging"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:transmission-tower"
+
+    @property
+    def is_on(self):
+        return bool(((self.coordinator.data or {}).get("settings") or {}).get("gridChargingEnabled", False))
+
+    async def async_turn_on(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, True)
+        await self.coordinator.async_request_refresh()
+
+    async def async_turn_off(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, False)
+        await self.coordinator.async_request_refresh()
+
+
+class SolarOfThingsGridFeedInSwitch(_BaseSwitch):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Grid Feed-In"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_feed_in"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:transmission-tower-export"
+
+    @property
+    def is_on(self):
+        return bool(((self.coordinator.data or {}).get("settings") or {}).get("gridFeedInEnabled", False))
+
+    async def async_turn_on(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, True)
+        await self.coordinator.async_request_refresh()
+
+    async def async_turn_off(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, False)
+        await self.coordinator.async_request_refresh()
+
+
+class SolarOfThingsBackupModeSwitch(_BaseSwitch):
+    def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
+        super().__init__(api, coordinator, station_id, device_id, device_name)
+        self._attr_name = f"{device_name} Backup Mode"
+        self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_backup_mode"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:battery-lock"
+
+    @property
+    def is_on(self):
+        return bool(((self.coordinator.data or {}).get("settings") or {}).get("backupModeEnabled", False))
+
+    async def async_turn_on(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, True)
+        await self.coordinator.async_request_refresh()
+
+    async def async_turn_off(self, **kwargs):
+        await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, False)
+        await self.coordinator.async_request_refresh()

+ 84 - 0
custom_components/solar_of_things/translations/en.json

@@ -0,0 +1,84 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "title": "Add Solar of Things Station",
+        "description": "Configure your Solar of Things station. You can add multiple stations by configuring this integration multiple times.\n\nTo find your credentials:\n1. Log into https://solar.siseli.com\n2. Open browser developer tools (F12) → Network tab\n3. Refresh the page and look at API requests\n4. Copy the IOT-Token from request headers\n5. Find stationId and deviceId in the request URLs or payloads\n\nMore info: {docs_url}",
+        "data": {
+          "iot_token": "IOT Token",
+          "station_id": "Station ID (required)",
+          "device_id": "Device ID (optional; leave blank to auto-discover all devices)",
+          "time_zone": "Time zone (IOT-Time-Zone header)"
+        }
+      },
+      "add_another": {
+        "title": "Add Another Station?",
+        "description": "You have added {station_count} station(s). Would you like to add another?",
+        "data": {
+          "add_another": "Add another station"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "Failed to connect to Solar of Things API. Please check your IOT Token and IDs.",
+      "unknown": "Unexpected error occurred. Please check the logs."
+    },
+    "abort": {
+      "already_configured": "This station is already configured."
+    }
+  },
+  "options": {
+    "step": {
+      "init": {
+        "title": "Solar of Things Options",
+        "data": {
+          "update_interval": "Update interval (minutes)"
+        }
+      }
+    }
+  },
+  "entity": {
+    "number": {
+      "battery_charge_limit": {
+        "name": "Battery Charge Limit"
+      },
+      "battery_discharge_limit": {
+        "name": "Battery Discharge Limit"
+      },
+      "grid_charge_limit": {
+        "name": "Grid Charge Limit"
+      }
+    },
+    "select": {
+      "operating_mode": {
+        "name": "Operating Mode",
+        "state": {
+          "self_use": "Self-Use",
+          "time_of_use": "Time-of-Use",
+          "backup": "Backup",
+          "grid_tie": "Grid-Tie",
+          "off_grid": "Off-Grid"
+        }
+      },
+      "battery_priority": {
+        "name": "Battery Priority",
+        "state": {
+          "solar_first": "Solar First",
+          "battery_first": "Battery First",
+          "grid_first": "Grid First"
+        }
+      }
+    },
+    "switch": {
+      "grid_charging": {
+        "name": "Grid Charging"
+      },
+      "grid_feed_in": {
+        "name": "Grid Feed-In"
+      },
+      "backup_mode": {
+        "name": "Backup Mode"
+      }
+    }
+  }
+}

+ 8 - 0
hacs.json

@@ -0,0 +1,8 @@
+{
+  "name": "Solar of Things",
+  "render_readme": true,
+  "domains": ["sensor", "number", "select", "switch"],
+  "homeassistant": "2023.1.0",
+  "zip_release": true,
+  "filename": "solar-of-things-ha.zip"
+}