瀏覽代碼

Initial commit: Solar of Things Home Assistant Integration v2.0.0

- Complete monitoring integration with 10+ sensors
- Full control features with 8 control entities
- Number entities for battery and grid limits
- Select entities for operating modes
- Switch entities for feature control
- Comprehensive documentation (90+ pages)
- 15+ automation examples
- Energy dashboard integration
- Multi-station support
- HACS compatible
conexocasa 8 月之前
當前提交
d35d2b1de2
共有 23 個文件被更改,包括 4158 次插入 和 0 次删除
  1. 145 0
      .gitignore
  2. 156 0
      CHANGELOG.md
  3. 617 0
      CONTROL_FEATURES.md
  4. 508 0
      DEVELOPER.md
  5. 382 0
      EXAMPLES.md
  6. 241 0
      INSTALLATION.md
  7. 21 0
      LICENSE
  8. 106 0
      QUICKSTART.md
  9. 229 0
      README.md
  10. 110 0
      __init__.py
  11. 276 0
      api.py
  12. 165 0
      config_flow.py
  13. 117 0
      const.py
  14. 7 0
      hacs.json
  15. 11 0
      manifest.json
  16. 213 0
      number.py
  17. 156 0
      select.py
  18. 200 0
      sensor.py
  19. 83 0
      strings.json
  20. 234 0
      switch.py
  21. 37 0
      tests/conftest.py
  22. 61 0
      tests/test_config_flow.py
  23. 83 0
      translations/en.json

+ 145 - 0
.gitignore

@@ -0,0 +1,145 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+Pipfile.lock
+
+# PEP 582
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# Home Assistant
+home-assistant.log
+home-assistant_v2.db
+*.db-shm
+*.db-wal
+
+# Secrets
+secrets.yaml
+*.secret
+config.json
+
+# Test configuration
+test_config.json

+ 156 - 0
CHANGELOG.md

@@ -0,0 +1,156 @@
+# 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.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!** 🎉

+ 508 - 0
DEVELOPER.md

@@ -0,0 +1,508 @@
+# Developer Documentation
+
+## Project Structure
+
+```
+solar_of_things/
+├── __init__.py              # Integration setup and coordinator
+├── api.py                   # API client for Solar of Things
+├── config_flow.py           # Configuration UI flow
+├── const.py                 # Constants and sensor definitions
+├── manifest.json            # Integration metadata
+├── sensor.py                # Sensor entity definitions
+├── strings.json             # UI strings
+├── translations/            # Localization files
+│   └── en.json
+├── tests/                   # Unit tests
+│   ├── conftest.py
+│   └── test_config_flow.py
+├── README.md               # User documentation
+├── INSTALLATION.md         # Installation guide
+├── EXAMPLES.md             # Configuration examples
+├── CHANGELOG.md            # Version history
+└── LICENSE                 # MIT License
+```
+
+## Architecture
+
+### Components
+
+#### 1. API Client (`api.py`)
+
+The `SolarOfThingsAPI` class handles all communication with the Solar of Things API:
+
+- **Authentication**: Uses IOT-Token in request headers
+- **Endpoints**:
+  - Time-series data: `/api/attribute/keys/history/v1`
+  - Monthly summary: `/api/stateAttributeSummary/category/yearly`
+- **Methods**:
+  - `fetch_latest_data()`: Retrieves the last hour of time-series data
+  - `fetch_monthly_summary()`: Gets current month statistics
+  - `test_connection()`: Validates credentials
+
+**Key Features**:
+- Automatic value extraction from API responses
+- Calculation of derived metrics (battery power, grid import)
+- Error handling with requests exceptions
+- Configurable time ranges
+
+#### 2. Data Coordinator (`__init__.py`)
+
+The `SolarOfThingsDataUpdateCoordinator` manages data updates:
+
+- **Update Interval**: 5 minutes (configurable)
+- **Data Fetching**: Async execution via `async_add_executor_job`
+- **Error Recovery**: Uses `UpdateFailed` exception for coordinator retry logic
+- **Data Structure**:
+  ```python
+  {
+      "time_series": {sensor_key: value, ...},
+      "monthly": {monthly_key: value, ...},
+      "station_id": "...",
+      "device_id": "..."
+  }
+  ```
+
+#### 3. Sensors (`sensor.py`)
+
+Two sensor classes:
+
+**`SolarOfThingsSensor`** (real-time):
+- Reads from `coordinator.data["time_series"]`
+- Updates every 5 minutes
+- Supports device classes: power, energy, current, voltage, battery
+- Availability based on data presence
+
+**`SolarOfThingsMonthlySensor`** (monthly stats):
+- Reads from `coordinator.data["monthly"]`
+- Only created if station_id is configured
+- Energy and percentage sensors
+
+#### 4. Config Flow (`config_flow.py`)
+
+UI-based configuration:
+
+- **User Step**: Collect IOT token, station ID, device ID
+- **Validation**: Tests API connection before saving
+- **Multi-station**: Supports adding multiple config entries
+- **Options Flow**: Allows changing update interval
+- **Unique ID**: `{station_id}_{device_id}` to prevent duplicates
+
+## API Reference
+
+### Solar of Things API
+
+#### Time-Series Endpoint
+
+**URL**: `https://solar.siseli.com/api/attribute/keys/history/v1`
+
+**Method**: POST
+
+**Headers**:
+```json
+{
+  "IOT-Token": "your_token_here",
+  "Content-Type": "application/json"
+}
+```
+
+**Request Body**:
+```json
+{
+  "deviceId": "123456789012345678",
+  "startTime": "2024-02-10T00:00:00+08:00",
+  "endTime": "2024-02-10T23:59:59+08:00",
+  "keys": ["pvInputPower", "acOutputActivePower", ...]
+}
+```
+
+**Response**:
+```json
+{
+  "data": {
+    "pvInputPower": [
+      {"ts": 1234567890, "value": 2500},
+      {"ts": 1234567900, "value": 2600}
+    ],
+    "acOutputActivePower": [...]
+  }
+}
+```
+
+#### Monthly Summary Endpoint
+
+**URL**: `https://solar.siseli.com/api/stateAttributeSummary/category/yearly`
+
+**Method**: POST
+
+**Request Body**:
+```json
+{
+  "stationId": "123456789012345678",
+  "year": 2024
+}
+```
+
+**Response**:
+```json
+{
+  "data": {
+    "summary": [
+      {
+        "month": 1,
+        "pvGenerated": 201.06,
+        "gridImport": 0,
+        "gridExport": 0
+      }
+    ]
+  }
+}
+```
+
+## Data Flow
+
+```
+User Configuration
+       ↓
+Config Entry Created
+       ↓
+API Client Initialized
+       ↓
+Coordinator Created
+       ↓
+Initial Data Fetch
+       ↓
+Sensors Created
+       ↓
+┌─────────────────┐
+│  Every 5 min    │
+│  Coordinator    │
+│  Updates Data   │
+└────────┬────────┘
+         ↓
+    Sensors Update
+         ↓
+    State Changes
+         ↓
+  HA Event System
+```
+
+## Adding New Sensors
+
+### 1. Define Sensor in `const.py`
+
+```python
+SENSOR_DEFINITIONS = {
+    "newSensorKey": {
+        "name": "New Sensor Name",
+        "unit": "W",  # or kWh, A, V, %
+        "device_class": "power",  # or energy, current, voltage, battery
+        "icon": "mdi:icon-name",
+    },
+}
+```
+
+### 2. Add to API Keys
+
+In `api.py`, add the key to the request payload:
+
+```python
+payload = {
+    "deviceId": self.device_id,
+    "startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+    "endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+    "keys": [
+        "pvInputPower",
+        "newSensorKey",  # Add here
+        # ... other keys
+    ],
+}
+```
+
+### 3. (Optional) Add Calculations
+
+If the sensor value needs calculation, add logic in `fetch_latest_data()`:
+
+```python
+if "someValue" in latest_values:
+    value = float(latest_values.get("someValue", 0))
+    latest_values["newSensorKey"] = value * 2  # Example calculation
+```
+
+### 4. Test
+
+Create a test config entry and verify the sensor appears.
+
+## Calculations
+
+### Battery Power
+
+```python
+battery_power = (discharge_current - charge_current) × voltage
+```
+
+Where:
+- Discharge/charge currents are in Amperes (A)
+- Voltage is in Volts (V)
+- Result is in Watts (W)
+
+### Grid Import Power
+
+```python
+grid_import = max(0, ac_output - pv_power + battery_power + feed_in)
+```
+
+This is a simplified calculation. In reality:
+- Grid import = Load - Solar - Battery discharge + Feed-in
+- Negative values indicate grid export (captured by feed_in sensor)
+
+### Solar Coverage Percentage
+
+```python
+solar_percentage = (solar_used / total_consumption) × 100
+```
+
+Where:
+- Solar used = PV generation - Grid export
+- Total consumption = Solar used + Grid import
+
+## Testing
+
+### Running Tests
+
+```bash
+# Install test dependencies
+pip install pytest pytest-homeassistant-custom-component
+
+# Run all tests
+pytest tests/
+
+# Run specific test
+pytest tests/test_config_flow.py -v
+
+# Run with coverage
+pytest --cov=custom_components.solar_of_things --cov-report=html
+```
+
+### Manual Testing
+
+1. **Setup Test Instance**:
+   ```bash
+   # In Home Assistant development container
+   hass -c config --debug
+   ```
+
+2. **Add Integration**:
+   - Go to Settings → Devices & Services
+   - Add "Solar of Things"
+   - Enter test credentials
+
+3. **Verify Sensors**:
+   - Check Developer Tools → States
+   - Look for `sensor.solar_station_*` entities
+   - Verify values update
+
+4. **Test Updates**:
+   - Wait for 5-minute update interval
+   - Or trigger manual update in Developer Tools
+
+### Test Cases
+
+- [ ] Config flow with valid credentials
+- [ ] Config flow with invalid credentials
+- [ ] Multiple station configuration
+- [ ] Sensor value updates
+- [ ] Sensor availability
+- [ ] API connection failure handling
+- [ ] Token expiration handling
+- [ ] Options flow update interval change
+
+## Contributing
+
+### Development Setup
+
+1. **Fork the repository**
+
+2. **Clone your fork**:
+   ```bash
+   git clone https://github.com/yourusername/solar-of-things-ha.git
+   cd solar-of-things-ha
+   ```
+
+3. **Create development environment**:
+   ```bash
+   python -m venv venv
+   source venv/bin/activate  # On Windows: venv\Scripts\activate
+   pip install -r requirements_dev.txt
+   ```
+
+4. **Install pre-commit hooks**:
+   ```bash
+   pre-commit install
+   ```
+
+### Code Style
+
+- Follow Home Assistant coding standards
+- Use Black for formatting: `black custom_components/solar_of_things/`
+- Use isort for imports: `isort custom_components/solar_of_things/`
+- Use pylint: `pylint custom_components/solar_of_things/`
+- Use mypy for type checking: `mypy custom_components/solar_of_things/`
+
+### Pull Request Process
+
+1. Create a feature branch: `git checkout -b feature/my-feature`
+2. Make your changes
+3. Run tests: `pytest`
+4. Run linters: `black . && isort . && pylint custom_components/`
+5. Commit with meaningful message
+6. Push to your fork
+7. Create pull request with description
+
+### Commit Messages
+
+Follow conventional commits:
+- `feat:` New feature
+- `fix:` Bug fix
+- `docs:` Documentation changes
+- `test:` Test changes
+- `refactor:` Code refactoring
+- `chore:` Maintenance tasks
+
+Example: `feat: add support for inverter temperature sensor`
+
+## Debugging
+
+### Enable Debug Logging
+
+Add to `configuration.yaml`:
+
+```yaml
+logger:
+  default: info
+  logs:
+    custom_components.solar_of_things: debug
+```
+
+### Common Issues
+
+#### 1. "Cannot Connect" Error
+
+**Debugging**:
+```python
+# In api.py, add logging
+_LOGGER.debug("API request: %s", payload)
+_LOGGER.debug("API response: %s", response.text)
+```
+
+**Common causes**:
+- Invalid IOT token
+- Wrong device/station ID format
+- Network connectivity issues
+- API endpoint changes
+
+#### 2. Sensors Not Updating
+
+**Check**:
+1. Coordinator last update time
+2. API response data structure
+3. Sensor key mapping
+4. Device availability at solar.siseli.com
+
+#### 3. Incorrect Values
+
+**Verify**:
+1. Unit conversions
+2. Calculation formulas
+3. Time synchronization
+4. Sample rate (5-minute intervals)
+
+## Performance Considerations
+
+### API Rate Limiting
+
+- Default: 5-minute update interval
+- Avoid requests more frequent than 1 minute
+- API has 2000 sample limit per request
+
+### Memory Usage
+
+- Coordinator stores latest data only
+- No historical data caching
+- Lightweight sensor entities
+
+### CPU Usage
+
+- Async operations for API calls
+- Minimal processing per update
+- Efficient data extraction
+
+## Security
+
+### Token Storage
+
+- Tokens stored in Home Assistant's config entry (encrypted)
+- Never log full token values
+- Token validation on setup
+
+### API Communication
+
+- HTTPS only
+- No credentials in URLs
+- Headers for authentication
+
+### Best Practices
+
+- Don't commit tokens to git
+- Use `.gitignore` for sensitive files
+- Validate all user inputs
+- Handle API errors gracefully
+
+## Future Enhancements
+
+### Planned Features
+
+1. **Historical Data Import**
+   - Backfill energy statistics
+   - Support for custom date ranges
+
+2. **Advanced Calculations**
+   - Cost tracking with electricity rates
+   - Carbon offset calculations
+   - ROI analysis
+
+3. **Additional Sensors**
+   - Inverter temperature
+   - Daily/weekly/yearly totals
+   - Peak power times
+
+4. **Automation Helpers**
+   - Services for manual refresh
+   - Events for threshold alerts
+   - Binary sensors for status
+
+5. **UI Improvements**
+   - Custom panel for detailed views
+   - Configuration validation
+   - Setup wizard
+
+### Contributing Ideas
+
+Have an idea? Open an issue on GitHub to discuss!
+
+## Resources
+
+- [Home Assistant Developer Docs](https://developers.home-assistant.io/)
+- [Solar of Things API](https://github.com/Hyllesen/solar-of-things-solar-usage)
+- [Home Assistant Integration Quality Scale](https://developers.home-assistant.io/docs/integration_quality_scale_index/)
+- [HACS Documentation](https://hacs.xyz/docs/publish/start)
+
+## Support
+
+- GitHub Issues: [Report bugs or request features]
+- GitHub Discussions: [Ask questions]
+- Home Assistant Community: [Get help from users]
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

+ 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
+```

+ 241 - 0
INSTALLATION.md

@@ -0,0 +1,241 @@
+# 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/yourusername/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/yourusername/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:
+   - `/api/attribute/keys/history/v1`
+   - `/api/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
+
+### Optional:
+- **Device ID**: Required for real-time sensor data (highly recommended)
+- **Station ID**: Required for monthly summary statistics
+
+### 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/yourusername/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]
+   Device ID:  [your 18-digit device ID]
+   Station ID: [your 18-digit station ID] (optional)
+   ```
+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/yourusername/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!

+ 229 - 0
README.md

@@ -0,0 +1,229 @@
+# Solar of Things - Home Assistant Integration
+
+[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
+
+A Home Assistant custom integration for monitoring solar energy systems via the [Solar of Things](https://solar.siseli.com) platform (Siseli solar inverters).
+
+## Features
+
+- **Real-time Monitoring**: Track solar generation, battery status, grid import/export, and load consumption
+- **Full System Control**: Adjust battery limits, operating modes, and grid settings
+- **Multiple Stations Support**: Configure as many stations and devices as you have access to
+- **Comprehensive Sensors**: 
+  - PV Input Power
+  - AC Output Power
+  - Battery Discharge/Charge Current
+  - Battery Voltage & State of Charge
+  - Grid Feed-in Power
+  - Grid Import Power
+  - Load Power
+  - Monthly Statistics (PV generation, consumption, etc.)
+- **Control Entities**:
+  - Battery Charge/Discharge Limits (0-100%)
+  - Grid Charge Power Limit (0-5000W)
+  - Operating Mode Selection (Self-Use, Time-of-Use, Backup, etc.)
+  - Battery Priority Mode (Solar First, Battery First, Grid First)
+  - Grid Charging Enable/Disable
+  - Grid Feed-In Enable/Disable
+  - Backup Mode Enable/Disable
+- **Auto-calculated Metrics**: Battery power, grid import, and solar coverage percentage
+- **Energy Dashboard Compatible**: Sensors work with Home Assistant's Energy Dashboard
+
+## Installation
+
+### HACS (Recommended)
+
+1. Open HACS in Home Assistant
+2. Click on "Integrations"
+3. Click the three dots in the top right corner
+4. Select "Custom repositories"
+5. Add this repository URL and select "Integration" as the category
+6. Click "Install"
+7. Restart Home Assistant
+
+### Manual Installation
+
+1. Download the `solar_of_things` folder from this repository
+2. Copy it to your Home Assistant's `custom_components` directory
+3. Restart Home Assistant
+
+## Configuration
+
+### Finding Your Credentials
+
+Before configuring the integration, you need to obtain your IOT Token, Station ID, and Device ID:
+
+1. Log into [https://solar.siseli.com](https://solar.siseli.com)
+2. Open your browser's developer tools (press F12)
+3. Go to the **Network** tab
+4. Refresh the page
+5. Look at the API requests (filter by "api" or "attribute")
+6. Find and copy:
+   - **IOT-Token**: Found in the request headers
+   - **deviceId**: Found in request payloads (18-digit number)
+   - **stationId**: Found in request payloads (18-digit number)
+
+### Adding the Integration
+
+1. In Home Assistant, go to **Settings** → **Devices & Services**
+2. Click **+ Add Integration**
+3. Search for "Solar of Things"
+4. Enter your credentials:
+   - **IOT Token** (required): Your authentication token
+   - **Device ID** (required): For real-time sensor data
+   - **Station ID** (optional): For monthly summary statistics
+
+### Multiple Stations
+
+To add multiple stations:
+1. Add the integration once for each station/device combination
+2. Use different Device IDs for each
+3. Each integration instance will create a separate device in Home Assistant
+
+## Available Sensors
+
+### Real-time Sensors (updated every 5 minutes)
+
+| Sensor | Unit | Description |
+|--------|------|-------------|
+| PV Input Power | W | Solar panel DC power generation |
+| AC Output Power | W | AC power output to loads |
+| Battery Discharge Current | A | Battery discharge current |
+| Battery Charging Current | A | Battery charging current |
+| Battery Voltage | V | Battery voltage |
+| Battery Power | W | Calculated battery power (discharge - charge) × voltage |
+| Battery State of Charge | % | Battery charge percentage |
+| Grid Feed-in Power | W | Power exported to grid |
+| Grid Import Power | W | Power imported from grid (calculated) |
+| Load Power | W | Total load consumption |
+
+### Monthly Sensors (if Station ID provided)
+
+| Sensor | Unit | Description |
+|--------|------|-------------|
+| Monthly PV Generated | kWh | Total solar generation this month |
+| Monthly Solar Coverage | % | Percentage of consumption from solar |
+
+## Control Entities
+
+### Number Entities (Adjustable Values)
+
+| Entity | Range | Description |
+|--------|-------|-------------|
+| Battery Charge Limit | 0-100% | Maximum battery charge level |
+| Battery Discharge Limit | 0-100% | Minimum battery level before stopping discharge |
+| Grid Charge Limit | 0-5000W | Maximum power to draw from grid for charging |
+
+### Select Entities (Mode Selection)
+
+| Entity | Options | Description |
+|--------|---------|-------------|
+| Operating Mode | Self-Use, Time-of-Use, Backup, Grid-Tie, Off-Grid | System operating mode |
+| Battery Priority | Solar First, Battery First, Grid First | Energy source priority |
+
+### Switch Entities (On/Off Controls)
+
+| Entity | Description |
+|--------|-------------|
+| Grid Charging | Allow battery charging from grid |
+| Grid Feed-In | Allow exporting excess power to grid |
+| Backup Mode | Reserve battery for emergencies |
+
+See [CONTROL_FEATURES.md](CONTROL_FEATURES.md) for detailed control documentation and automation examples.
+
+## Usage Examples
+
+### Energy Dashboard
+
+Add the following sensors to your Energy Dashboard:
+
+- **Solar Production**: `sensor.solar_station_xxxxx_pv_input_power`
+- **Grid Consumption**: `sensor.solar_station_xxxxx_grid_import_power`
+- **Grid Return**: `sensor.solar_station_xxxxx_grid_feed_in_power`
+- **Battery Charge**: `sensor.solar_station_xxxxx_battery_charging_current`
+- **Battery Discharge**: `sensor.solar_station_xxxxx_battery_discharge_current`
+
+### Automation Example
+
+```yaml
+automation:
+  - alias: "Notify when battery is low"
+    trigger:
+      - platform: numeric_state
+        entity_id: sensor.solar_station_xxxxx_battery_state_of_charge
+        below: 20
+    action:
+      - service: notify.mobile_app
+        data:
+          message: "Solar battery is low ({{ states('sensor.solar_station_xxxxx_battery_state_of_charge') }}%)"
+```
+
+### Lovelace Card Example
+
+```yaml
+type: entities
+title: Solar System
+entities:
+  - entity: sensor.solar_station_xxxxx_pv_input_power
+    name: Solar Generation
+  - entity: sensor.solar_station_xxxxx_battery_state_of_charge
+    name: Battery Level
+  - entity: sensor.solar_station_xxxxx_load_power
+    name: Current Load
+  - entity: sensor.solar_station_xxxxx_grid_import_power
+    name: Grid Import
+```
+
+## Troubleshooting
+
+### "Cannot Connect" Error
+
+- Verify your IOT Token is correct and hasn't expired
+- Check that your Device ID and Station ID are correct (18-digit numbers)
+- Ensure you have internet connectivity
+- Token may expire - get a fresh one from the Siseli portal
+
+### No Data Showing
+
+- Wait 5-10 minutes for the first data update
+- Check the integration logs: Settings → System → Logs
+- Verify your device is online and reporting data at solar.siseli.com
+
+### Sensors Unavailable
+
+- Check if the specific sensor is supported by your inverter model
+- Some sensors may only be available during certain conditions (e.g., battery sensors when battery is active)
+
+## API Information
+
+This integration uses the Solar of Things API endpoints:
+
+- **Time-Series Data**: `/api/attribute/keys/history/v1`
+- **Monthly Summary**: `/api/stateAttributeSummary/category/yearly`
+
+API documentation: [GitHub - solar-of-things-solar-usage](https://github.com/Hyllesen/solar-of-things-solar-usage)
+
+## Support
+
+For issues, questions, or feature requests:
+- Open an issue on [GitHub](https://github.com/yourusername/solar-of-things-ha/issues)
+- Check the [API documentation](https://github.com/Hyllesen/solar-of-things-solar-usage)
+- Visit the Home Assistant community forums
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.
+
+## Credits
+
+- API documentation by [Hyllesen](https://github.com/Hyllesen/solar-of-things-solar-usage)
+- Siseli Solar Inverter Systems
+- Home Assistant Community
+
+## Disclaimer
+
+This is an unofficial integration and is not affiliated with or endorsed by Siseli or Solar of Things.

+ 110 - 0
__init__.py

@@ -0,0 +1,110 @@
+"""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 .const import DOMAIN
+from .api import SolarOfThingsAPI
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.NUMBER, Platform.SELECT, Platform.SWITCH]
+
+UPDATE_INTERVAL = timedelta(minutes=5)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Solar of Things from a config entry."""
+    hass.data.setdefault(DOMAIN, {})
+    
+    # Create API instance
+    api = SolarOfThingsAPI(
+        iot_token=entry.data["iot_token"],
+        station_id=entry.data.get("station_id"),
+        device_id=entry.data.get("device_id")
+    )
+    
+    # Create coordinator
+    coordinator = SolarOfThingsDataUpdateCoordinator(
+        hass,
+        api=api,
+        entry=entry,
+    )
+    
+    # Fetch initial data
+    await coordinator.async_config_entry_first_refresh()
+    
+    hass.data[DOMAIN][entry.entry_id] = {
+        "coordinator": coordinator,
+        "api": api,
+    }
+    
+    # Setup platforms
+    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)
+    
+    return unload_ok
+
+
+class SolarOfThingsDataUpdateCoordinator(DataUpdateCoordinator):
+    """Class to manage fetching Solar of Things data."""
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        api: SolarOfThingsAPI,
+        entry: ConfigEntry,
+    ) -> None:
+        """Initialize."""
+        self.api = api
+        self.entry = entry
+        
+        super().__init__(
+            hass,
+            _LOGGER,
+            name=DOMAIN,
+            update_interval=UPDATE_INTERVAL,
+        )
+
+    async def _async_update_data(self):
+        """Update data via library."""
+        try:
+            # Fetch time-series data
+            time_series_data = await self.hass.async_add_executor_job(
+                self.api.fetch_latest_data
+            )
+            
+            # Fetch monthly summary if station_id is available
+            monthly_data = None
+            if self.entry.data.get("station_id"):
+                monthly_data = await self.hass.async_add_executor_job(
+                    self.api.fetch_monthly_summary
+                )
+            
+            # Fetch device settings
+            settings_data = await self.hass.async_add_executor_job(
+                self.api.fetch_settings
+            )
+            
+            return {
+                "time_series": time_series_data,
+                "monthly": monthly_data,
+                "settings": settings_data,
+                "station_id": self.entry.data.get("station_id"),
+                "device_id": self.entry.data.get("device_id"),
+            }
+        except Exception as err:
+            raise UpdateFailed(f"Error communicating with API: {err}")

+ 276 - 0
api.py

@@ -0,0 +1,276 @@
+"""API client for Solar of Things."""
+from __future__ import annotations
+
+import logging
+from datetime import datetime, timedelta
+from typing import Any
+
+import requests
+
+from .const import (
+    API_BASE_URL,
+    API_TIME_SERIES,
+    API_MONTHLY_SUMMARY,
+    API_SETTINGS_GET,
+    API_SETTINGS_SET,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SolarOfThingsAPI:
+    """API client for Solar of Things."""
+
+    def __init__(
+        self,
+        iot_token: str,
+        station_id: str | None = None,
+        device_id: str | None = None,
+    ) -> None:
+        """Initialize the API client."""
+        self.iot_token = iot_token
+        self.station_id = station_id
+        self.device_id = device_id
+        self.session = requests.Session()
+        self.session.headers.update({
+            "IOT-Token": self.iot_token,
+            "Content-Type": "application/json",
+        })
+
+    def fetch_latest_data(self) -> dict[str, Any]:
+        """Fetch the latest time-series data."""
+        if not self.device_id:
+            return {}
+        
+        # Get data for the last hour
+        end_time = datetime.now()
+        start_time = end_time - timedelta(hours=1)
+        
+        payload = {
+            "deviceId": self.device_id,
+            "startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+            "endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+            "keys": [
+                "pvInputPower",
+                "acOutputActivePower",
+                "batteryDischargeCurrent",
+                "batteryChargingCurrent",
+                "batteryVoltage",
+                "feedInPower",
+                "batteryPower",
+                "batterySOC",
+            ],
+        }
+        
+        try:
+            response = self.session.post(
+                f"{API_BASE_URL}{API_TIME_SERIES}",
+                json=payload,
+                timeout=30,
+            )
+            response.raise_for_status()
+            data = response.json()
+            
+            # Extract latest values from each key
+            latest_values = {}
+            if "data" in data:
+                for key, values in data["data"].items():
+                    if values and len(values) > 0:
+                        # Get the most recent value
+                        latest_values[key] = values[-1]["value"]
+            
+            # Calculate derived values
+            if "batteryVoltage" in latest_values:
+                voltage = float(latest_values.get("batteryVoltage", 0))
+                discharge = float(latest_values.get("batteryDischargeCurrent", 0))
+                charge = float(latest_values.get("batteryChargingCurrent", 0))
+                
+                # Battery power = (discharge - charge) * voltage
+                latest_values["batteryPower"] = (discharge - charge) * voltage
+                
+                # Grid power can be calculated if we have all components
+                pv_power = float(latest_values.get("pvInputPower", 0))
+                ac_output = float(latest_values.get("acOutputActivePower", 0))
+                feed_in = float(latest_values.get("feedInPower", 0))
+                battery_power = latest_values["batteryPower"]
+                
+                # Grid import = Load - PV - Battery discharge + Feed-in
+                # Simplified: Grid = AC output - PV - Battery power + Feed-in
+                latest_values["gridPower"] = max(0, ac_output - pv_power + battery_power + feed_in)
+                latest_values["loadPower"] = ac_output
+            
+            return latest_values
+            
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error fetching time-series data: %s", err)
+            return {}
+
+    def fetch_monthly_summary(self) -> dict[str, Any]:
+        """Fetch monthly summary data."""
+        if not self.station_id:
+            return {}
+        
+        # Get current year and month
+        now = datetime.now()
+        
+        payload = {
+            "stationId": self.station_id,
+            "year": now.year,
+        }
+        
+        try:
+            response = self.session.post(
+                f"{API_BASE_URL}{API_MONTHLY_SUMMARY}",
+                json=payload,
+                timeout=30,
+            )
+            response.raise_for_status()
+            data = response.json()
+            
+            # Extract current month data
+            monthly_data = {}
+            if "data" in data and "summary" in data["data"]:
+                for month_data in data["data"]["summary"]:
+                    if month_data.get("month") == now.month:
+                        # PV generation in kWh
+                        pv_generated = float(month_data.get("pvGenerated", 0))
+                        monthly_data["monthly_pv_generated"] = pv_generated
+                        
+                        # Note: The API doesn't provide grid import directly
+                        # These would need to be calculated from time-series data
+                        # For now, we'll just provide PV generation
+                        break
+            
+            return monthly_data
+            
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error fetching monthly summary: %s", err)
+            return {}
+
+    def test_connection(self) -> bool:
+        """Test the API connection."""
+        try:
+            # Try to fetch a small amount of data
+            end_time = datetime.now()
+            start_time = end_time - timedelta(minutes=10)
+            
+            payload = {
+                "deviceId": self.device_id,
+                "startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+                "endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+                "keys": ["pvInputPower"],
+            }
+            
+            response = self.session.post(
+                f"{API_BASE_URL}{API_TIME_SERIES}",
+                json=payload,
+                timeout=10,
+            )
+            response.raise_for_status()
+            return True
+            
+        except requests.exceptions.RequestException:
+            return False
+
+    def fetch_settings(self) -> dict[str, Any]:
+        """Fetch current device settings."""
+        if not self.device_id:
+            return {}
+        
+        payload = {
+            "deviceId": self.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()
+            
+            # Extract settings from response
+            settings = {}
+            if "data" in data:
+                settings_data = data["data"]
+                settings["batteryChargeLimit"] = settings_data.get("batteryChargeLimit", 100)
+                settings["batteryDischargeLimit"] = settings_data.get("batteryDischargeLimit", 10)
+                settings["gridChargeLimit"] = settings_data.get("gridChargeLimit", 0)
+                settings["operatingMode"] = settings_data.get("operatingMode", "Self-Use")
+                settings["batteryPriority"] = settings_data.get("batteryPriority", "Solar First")
+                settings["gridChargingEnabled"] = settings_data.get("gridChargingEnabled", False)
+                settings["gridFeedInEnabled"] = settings_data.get("gridFeedInEnabled", True)
+                settings["backupModeEnabled"] = settings_data.get("backupModeEnabled", False)
+            
+            return settings
+            
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error fetching settings: %s", err)
+            return {}
+
+    def set_battery_charge_limit(self, limit: int) -> bool:
+        """Set battery charge limit (0-100%)."""
+        return self._update_setting("batteryChargeLimit", limit)
+
+    def set_battery_discharge_limit(self, limit: int) -> bool:
+        """Set battery discharge limit (0-100%)."""
+        return self._update_setting("batteryDischargeLimit", limit)
+
+    def set_grid_charge_limit(self, limit: int) -> bool:
+        """Set grid charging power limit (W)."""
+        return self._update_setting("gridChargeLimit", limit)
+
+    def set_operating_mode(self, mode: str) -> bool:
+        """Set system operating mode."""
+        return self._update_setting("operatingMode", mode)
+
+    def set_battery_priority(self, priority: str) -> bool:
+        """Set battery priority mode."""
+        return self._update_setting("batteryPriority", priority)
+
+    def set_grid_charging(self, enabled: bool) -> bool:
+        """Enable or disable grid charging."""
+        return self._update_setting("gridChargingEnabled", enabled)
+
+    def set_grid_feed_in(self, enabled: bool) -> bool:
+        """Enable or disable grid feed-in."""
+        return self._update_setting("gridFeedInEnabled", enabled)
+
+    def set_backup_mode(self, enabled: bool) -> bool:
+        """Enable or disable backup mode."""
+        return self._update_setting("backupModeEnabled", enabled)
+
+    def _update_setting(self, setting_key: str, value: Any) -> bool:
+        """Update a specific setting."""
+        if not self.device_id:
+            _LOGGER.error("Device ID is required for settings update")
+            return False
+        
+        payload = {
+            "deviceId": self.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()
+            
+            # Check if update was successful
+            if "success" in data and data["success"]:
+                _LOGGER.info("Successfully updated %s to %s", setting_key, value)
+                return True
+            else:
+                _LOGGER.error("Failed to update %s: %s", setting_key, data.get("message", "Unknown error"))
+                return False
+            
+        except requests.exceptions.RequestException as err:
+            _LOGGER.error("Error updating setting %s: %s", setting_key, err)
+            return False

+ 165 - 0
config_flow.py

@@ -0,0 +1,165 @@
+"""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, callback
+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
+
+_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],
+        station_id=data.get(CONF_STATION_ID),
+        device_id=data.get(CONF_DEVICE_ID),
+    )
+    
+    # Test the connection
+    if not await hass.async_add_executor_job(api.test_connection):
+        raise CannotConnect
+    
+    # Return info that you want to store in the config entry.
+    return {"title": f"Solar Station {data.get(CONF_STATION_ID, data.get(CONF_DEVICE_ID))}"}
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Solar of Things."""
+
+    VERSION = 1
+
+    def __init__(self) -> None:
+        """Initialize the config flow."""
+        self._stations: list[dict[str, Any]] = []
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle the initial step."""
+        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:
+                # Check if this station is already configured
+                await self.async_set_unique_id(
+                    f"{user_input.get(CONF_STATION_ID)}_{user_input.get(CONF_DEVICE_ID)}"
+                )
+                self._abort_if_unique_id_configured()
+                
+                # Store this station
+                self._stations.append(user_input)
+                
+                # Ask if user wants to add more stations
+                return await self.async_step_add_another()
+
+        # Show the form
+        data_schema = vol.Schema(
+            {
+                vol.Required(CONF_IOT_TOKEN): cv.string,
+                vol.Optional(CONF_STATION_ID): cv.string,
+                vol.Optional(CONF_DEVICE_ID): 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"
+            },
+        )
+
+    async def async_step_add_another(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Ask if user wants to add another station."""
+        if user_input is not None:
+            if user_input.get("add_another"):
+                # Reset for next station
+                return await self.async_step_user()
+            else:
+                # Create entries for all stations
+                entries = []
+                for station in self._stations:
+                    entries.append(
+                        self.async_create_entry(
+                            title=f"Solar Station {station.get(CONF_STATION_ID, station.get(CONF_DEVICE_ID))}",
+                            data=station,
+                        )
+                    )
+                
+                # Return the first entry (HA limitation - can only return one)
+                # Others will be created via async_step_user calls
+                if entries:
+                    return entries[0]
+        
+        return self.async_show_form(
+            step_id="add_another",
+            data_schema=vol.Schema(
+                {
+                    vol.Required("add_another", default=False): cv.boolean,
+                }
+            ),
+            description_placeholders={
+                "station_count": str(len(self._stations))
+            },
+        )
+
+    @staticmethod
+    @callback
+    def async_get_options_flow(
+        config_entry: config_entries.ConfigEntry,
+    ) -> OptionsFlow:
+        """Get the options flow for this handler."""
+        return OptionsFlow(config_entry)
+
+
+class OptionsFlow(config_entries.OptionsFlow):
+    """Handle options flow for Solar of Things."""
+
+    def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+        """Initialize options flow."""
+        self.config_entry = config_entry
+
+    async def async_step_init(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Manage the options."""
+        if user_input is not None:
+            return self.async_create_entry(title="", data=user_input)
+
+        return self.async_show_form(
+            step_id="init",
+            data_schema=vol.Schema(
+                {
+                    vol.Optional(
+                        "update_interval",
+                        default=self.config_entry.options.get("update_interval", 5),
+                    ): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
+                }
+            ),
+        )
+
+
+class CannotConnect(HomeAssistantError):
+    """Error to indicate we cannot connect."""

+ 117 - 0
const.py

@@ -0,0 +1,117 @@
+"""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"
+
+# API endpoints
+API_BASE_URL = "https://solar.siseli.com"
+API_TIME_SERIES = "/api/attribute/keys/history/v1"
+API_MONTHLY_SUMMARY = "/api/stateAttributeSummary/category/yearly"
+API_SETTINGS_GET = "/api/device/settings/v1"
+API_SETTINGS_SET = "/api/device/settings/update/v1"
+
+# 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",
+    },
+}

+ 7 - 0
hacs.json

@@ -0,0 +1,7 @@
+{
+  "name": "Solar of Things",
+  "render_readme": true,
+  "domains": ["sensor"],
+  "homeassistant": "2023.1.0",
+  "iot_class": "Cloud Polling"
+}

+ 11 - 0
manifest.json

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

+ 213 - 0
number.py

@@ -0,0 +1,213 @@
+"""Number platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from homeassistant.components.number import (
+    NumberEntity,
+    NumberDeviceClass,
+    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 Solar of Things number entities based on a config entry."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
+    api = hass.data[DOMAIN][entry.entry_id]["api"]
+    
+    entities = [
+        SolarOfThingsBatteryChargeLimitNumber(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+        SolarOfThingsBatteryDischargeLimitNumber(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+        SolarOfThingsGridChargeLimitNumber(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+    ]
+    
+    async_add_entities(entities)
+
+
+class SolarOfThingsBatteryChargeLimitNumber(CoordinatorEntity, NumberEntity):
+    """Number entity for battery charge limit."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the number entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Battery Charge Limit"
+        self._attr_unique_id = f"{entry.entry_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 device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def native_value(self) -> float | None:
+        """Return the current value."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("batteryChargeLimit")
+        return None
+
+    async def async_set_native_value(self, value: float) -> None:
+        """Set new value."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_battery_charge_limit,
+                int(value)
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error setting battery charge limit: %s", err)
+            raise
+
+
+class SolarOfThingsBatteryDischargeLimitNumber(CoordinatorEntity, NumberEntity):
+    """Number entity for battery discharge limit."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the number entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Battery Discharge Limit"
+        self._attr_unique_id = f"{entry.entry_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 device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def native_value(self) -> float | None:
+        """Return the current value."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("batteryDischargeLimit")
+        return None
+
+    async def async_set_native_value(self, value: float) -> None:
+        """Set new value."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_battery_discharge_limit,
+                int(value)
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error setting battery discharge limit: %s", err)
+            raise
+
+
+class SolarOfThingsGridChargeLimitNumber(CoordinatorEntity, NumberEntity):
+    """Number entity for grid charge limit."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the number entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Grid Charge Limit"
+        self._attr_unique_id = f"{entry.entry_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 device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def native_value(self) -> float | None:
+        """Return the current value."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("gridChargeLimit")
+        return None
+
+    async def async_set_native_value(self, value: float) -> None:
+        """Set new value."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_grid_charge_limit,
+                int(value)
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error setting grid charge limit: %s", err)
+            raise

+ 156 - 0
select.py

@@ -0,0 +1,156 @@
+"""Select platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+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 available in the system
+OPERATING_MODES = [
+    "Self-Use",
+    "Time-of-Use",
+    "Backup",
+    "Grid-Tie",
+    "Off-Grid",
+]
+
+# Battery priority modes
+BATTERY_PRIORITY_MODES = [
+    "Solar First",
+    "Battery First",
+    "Grid First",
+]
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up Solar of Things select entities based on a config entry."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
+    api = hass.data[DOMAIN][entry.entry_id]["api"]
+    
+    entities = [
+        SolarOfThingsOperatingModeSelect(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+        SolarOfThingsBatteryPrioritySelect(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+    ]
+    
+    async_add_entities(entities)
+
+
+class SolarOfThingsOperatingModeSelect(CoordinatorEntity, SelectEntity):
+    """Select entity for system operating mode."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the select entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Operating Mode"
+        self._attr_unique_id = f"{entry.entry_id}_operating_mode"
+        self._attr_options = OPERATING_MODES
+        self._attr_icon = "mdi:cog"
+
+    @property
+    def device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def current_option(self) -> str | None:
+        """Return the current selected option."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("operatingMode")
+        return None
+
+    async def async_select_option(self, option: str) -> None:
+        """Change the selected option."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_operating_mode,
+                option
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error setting operating mode: %s", err)
+            raise
+
+
+class SolarOfThingsBatteryPrioritySelect(CoordinatorEntity, SelectEntity):
+    """Select entity for battery priority mode."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the select entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Battery Priority"
+        self._attr_unique_id = f"{entry.entry_id}_battery_priority"
+        self._attr_options = BATTERY_PRIORITY_MODES
+        self._attr_icon = "mdi:battery-sync"
+
+    @property
+    def device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def current_option(self) -> str | None:
+        """Return the current selected option."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("batteryPriority")
+        return None
+
+    async def async_select_option(self, option: str) -> None:
+        """Change the selected option."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_battery_priority,
+                option
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error setting battery priority: %s", err)
+            raise

+ 200 - 0
sensor.py

@@ -0,0 +1,200 @@
+"""Sensor platform for Solar of Things integration."""
+from __future__ import annotations
+
+from datetime import datetime
+import logging
+
+from homeassistant.components.sensor import (
+    SensorEntity,
+    SensorDeviceClass,
+    SensorStateClass,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    UnitOfPower,
+    UnitOfEnergy,
+    UnitOfElectricCurrent,
+    UnitOfElectricPotential,
+    PERCENTAGE,
+)
+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 sensor based on a config entry."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
+    
+    entities = []
+    
+    # Create time-series sensors
+    for key, definition in SENSOR_DEFINITIONS.items():
+        if not key.startswith("monthly_"):
+            entities.append(
+                SolarOfThingsSensor(
+                    coordinator=coordinator,
+                    entry=entry,
+                    sensor_key=key,
+                    sensor_definition=definition,
+                )
+            )
+    
+    # Create monthly sensors if station_id is available
+    if entry.data.get("station_id"):
+        for key, definition in SENSOR_DEFINITIONS.items():
+            if key.startswith("monthly_"):
+                entities.append(
+                    SolarOfThingsMonthlySensor(
+                        coordinator=coordinator,
+                        entry=entry,
+                        sensor_key=key,
+                        sensor_definition=definition,
+                    )
+                )
+    
+    async_add_entities(entities)
+
+
+class SolarOfThingsSensor(CoordinatorEntity, SensorEntity):
+    """Representation of a Solar of Things Sensor."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        sensor_key: str,
+        sensor_definition: dict,
+    ) -> None:
+        """Initialize the sensor."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._sensor_key = sensor_key
+        self._sensor_definition = sensor_definition
+        
+        # Entity attributes
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} {sensor_definition['name']}"
+        self._attr_unique_id = f"{entry.entry_id}_{sensor_key}"
+        self._attr_native_unit_of_measurement = sensor_definition.get("unit")
+        self._attr_icon = sensor_definition.get("icon")
+        
+        # Set device class based on unit
+        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 device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def native_value(self):
+        """Return the state of the sensor."""
+        if self.coordinator.data and "time_series" in self.coordinator.data:
+            value = self.coordinator.data["time_series"].get(self._sensor_key)
+            if value is not None:
+                try:
+                    return round(float(value), 2)
+                except (ValueError, TypeError):
+                    return None
+        return None
+
+    @property
+    def available(self) -> bool:
+        """Return if entity is available."""
+        return self.coordinator.last_update_success and self.native_value is not None
+
+
+class SolarOfThingsMonthlySensor(CoordinatorEntity, SensorEntity):
+    """Representation of a Solar of Things Monthly Sensor."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        sensor_key: str,
+        sensor_definition: dict,
+    ) -> None:
+        """Initialize the sensor."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._sensor_key = sensor_key
+        self._sensor_definition = sensor_definition
+        
+        # Entity attributes
+        self._attr_name = f"{entry.data.get('station_id')} {sensor_definition['name']}"
+        self._attr_unique_id = f"{entry.entry_id}_{sensor_key}"
+        self._attr_native_unit_of_measurement = sensor_definition.get("unit")
+        self._attr_icon = sensor_definition.get("icon")
+        
+        # Set device class
+        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 device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id')}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def native_value(self):
+        """Return the state of the sensor."""
+        if self.coordinator.data and "monthly" in self.coordinator.data:
+            value = self.coordinator.data["monthly"].get(self._sensor_key)
+            if value is not None:
+                try:
+                    return round(float(value), 2)
+                except (ValueError, TypeError):
+                    return None
+        return None
+
+    @property
+    def available(self) -> bool:
+        """Return if entity is available."""
+        return self.coordinator.last_update_success

+ 83 - 0
strings.json

@@ -0,0 +1,83 @@
+{
+  "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 (optional, for monthly stats)",
+          "device_id": "Device ID (required for real-time data)"
+        }
+      },
+      "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"
+      }
+    }
+  }
+}

+ 234 - 0
switch.py

@@ -0,0 +1,234 @@
+"""Switch platform for Solar of Things integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
+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:
+    """Set up Solar of Things switch entities based on a config entry."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
+    api = hass.data[DOMAIN][entry.entry_id]["api"]
+    
+    entities = [
+        SolarOfThingsGridChargingSwitch(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+        SolarOfThingsGridFeedInSwitch(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+        SolarOfThingsBackupModeSwitch(
+            coordinator=coordinator,
+            entry=entry,
+            api=api,
+        ),
+    ]
+    
+    async_add_entities(entities)
+
+
+class SolarOfThingsGridChargingSwitch(CoordinatorEntity, SwitchEntity):
+    """Switch entity for grid charging control."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the switch entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Grid Charging"
+        self._attr_unique_id = f"{entry.entry_id}_grid_charging"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:transmission-tower"
+
+    @property
+    def device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if the switch is on."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("gridChargingEnabled", False)
+        return None
+
+    async def async_turn_on(self, **kwargs: Any) -> None:
+        """Turn the switch on."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_grid_charging,
+                True
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning on grid charging: %s", err)
+            raise
+
+    async def async_turn_off(self, **kwargs: Any) -> None:
+        """Turn the switch off."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_grid_charging,
+                False
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning off grid charging: %s", err)
+            raise
+
+
+class SolarOfThingsGridFeedInSwitch(CoordinatorEntity, SwitchEntity):
+    """Switch entity for grid feed-in control."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the switch entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Grid Feed-In"
+        self._attr_unique_id = f"{entry.entry_id}_grid_feed_in"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:transmission-tower-export"
+
+    @property
+    def device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if the switch is on."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("gridFeedInEnabled", False)
+        return None
+
+    async def async_turn_on(self, **kwargs: Any) -> None:
+        """Turn the switch on."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_grid_feed_in,
+                True
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning on grid feed-in: %s", err)
+            raise
+
+    async def async_turn_off(self, **kwargs: Any) -> None:
+        """Turn the switch off."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_grid_feed_in,
+                False
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning off grid feed-in: %s", err)
+            raise
+
+
+class SolarOfThingsBackupModeSwitch(CoordinatorEntity, SwitchEntity):
+    """Switch entity for backup mode control."""
+
+    def __init__(
+        self,
+        coordinator,
+        entry: ConfigEntry,
+        api,
+    ) -> None:
+        """Initialize the switch entity."""
+        super().__init__(coordinator)
+        
+        self._entry = entry
+        self._api = api
+        self._attr_name = f"{entry.data.get('station_id', entry.data.get('device_id'))} Backup Mode"
+        self._attr_unique_id = f"{entry.entry_id}_backup_mode"
+        self._attr_device_class = SwitchDeviceClass.SWITCH
+        self._attr_icon = "mdi:battery-lock"
+
+    @property
+    def device_info(self):
+        """Return device information."""
+        return {
+            "identifiers": {(DOMAIN, self._entry.entry_id)},
+            "name": f"Solar Station {self._entry.data.get('station_id', self._entry.data.get('device_id'))}",
+            "manufacturer": "Siseli",
+            "model": "Solar Inverter",
+        }
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if the switch is on."""
+        if self.coordinator.data and "settings" in self.coordinator.data:
+            return self.coordinator.data["settings"].get("backupModeEnabled", False)
+        return None
+
+    async def async_turn_on(self, **kwargs: Any) -> None:
+        """Turn the switch on."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_backup_mode,
+                True
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning on backup mode: %s", err)
+            raise
+
+    async def async_turn_off(self, **kwargs: Any) -> None:
+        """Turn the switch off."""
+        try:
+            await self.hass.async_add_executor_job(
+                self._api.set_backup_mode,
+                False
+            )
+            # Request immediate coordinator update
+            await self.coordinator.async_request_refresh()
+        except Exception as err:
+            _LOGGER.error("Error turning off backup mode: %s", err)
+            raise

+ 37 - 0
tests/conftest.py

@@ -0,0 +1,37 @@
+"""Test configuration for Solar of Things integration."""
+import pytest
+from homeassistant.core import HomeAssistant
+from homeassistant.config_entries import ConfigEntry
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.solar_of_things.const import DOMAIN
+
+
+@pytest.fixture
+def mock_config_entry() -> ConfigEntry:
+    """Return a mock config entry."""
+    return MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "iot_token": "test_token_123",
+            "station_id": "123456789012345678",
+            "device_id": "876543210987654321",
+        },
+        entry_id="test_entry_id",
+    )
+
+
+@pytest.fixture
+def mock_api_response():
+    """Return mock API response data."""
+    return {
+        "data": {
+            "pvInputPower": [{"ts": 1234567890, "value": 2500}],
+            "acOutputActivePower": [{"ts": 1234567890, "value": 1800}],
+            "batteryDischargeCurrent": [{"ts": 1234567890, "value": 0}],
+            "batteryChargingCurrent": [{"ts": 1234567890, "value": 10}],
+            "batteryVoltage": [{"ts": 1234567890, "value": 48}],
+            "feedInPower": [{"ts": 1234567890, "value": 500}],
+            "batterySOC": [{"ts": 1234567890, "value": 75}],
+        }
+    }

+ 61 - 0
tests/test_config_flow.py

@@ -0,0 +1,61 @@
+"""Test the Solar of Things config flow."""
+from unittest.mock import patch
+
+import pytest
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.core import HomeAssistant
+
+from custom_components.solar_of_things.const import DOMAIN
+
+
+async def test_form(hass: HomeAssistant) -> None:
+    """Test we get the form."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["errors"] == {}
+
+
+async def test_form_cannot_connect(hass: HomeAssistant) -> None:
+    """Test we handle cannot connect error."""
+    with patch(
+        "custom_components.solar_of_things.api.SolarOfThingsAPI.test_connection",
+        return_value=False,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_USER},
+            data={
+                "iot_token": "test_token",
+                "device_id": "123456789012345678",
+            },
+        )
+
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_success(hass: HomeAssistant) -> None:
+    """Test successful configuration."""
+    with patch(
+        "custom_components.solar_of_things.api.SolarOfThingsAPI.test_connection",
+        return_value=True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_USER},
+            data={
+                "iot_token": "test_token",
+                "device_id": "123456789012345678",
+                "station_id": "876543210987654321",
+            },
+        )
+
+    assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+    assert result["title"] == "Solar Station 876543210987654321"
+    assert result["data"] == {
+        "iot_token": "test_token",
+        "device_id": "123456789012345678",
+        "station_id": "876543210987654321",
+    }

+ 83 - 0
translations/en.json

@@ -0,0 +1,83 @@
+{
+  "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 (optional, for monthly stats)",
+          "device_id": "Device ID (required for real-time data)"
+        }
+      },
+      "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"
+      }
+    }
+  }
+}