Skip to content

Commit

Permalink
Support for switching shellies
Browse files Browse the repository at this point in the history
  • Loading branch information
mincequi committed Aug 31, 2023
1 parent fe9815f commit 639fe9d
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 120 deletions.
6 changes: 2 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ add_subdirectory(src/modbus)
add_subdirectory(src/mqtt)
add_subdirectory(src/things)
add_subdirectory(src/webapp)
add_subdirectory(src/websocket)
#add_subdirectory(src/ui)
add_subdirectory(src/webserver)

add_executable(elsewhere
src/main.cpp
Expand All @@ -46,8 +45,7 @@ target_link_libraries(elsewhere
modbus
mqtt
things
#ui
websocket
webserver
)

install(TARGETS elsewhere DESTINATION bin)
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
# ElsewhereEdge
# Iotic

ElsewhereEdge is a SunSpec - MQTT gateway. It collects measurement data from smart meters and solar inverters over Modbus TCP and exports via MQTT.
<img width="288" src="https://github.com/mincequi/ElsewhereEdge/assets/1805183/c5087dca-a473-4a1c-911a-faaf7f295660"> <img width="288" src="https://github.com/mincequi/ElsewhereEdge/assets/1805183/b4371fbd-d778-4dce-8918-90e0c6b39376">

Iotic is a multi-purpose IoT device manager which focuses on thermal and electrical energy.
Photovoltaic systems, electric vehicles and heatings can be brought together for smart energy management.
It also can act as a data collector and export to InfluxDB and MQTT. This allows multiple scenarios (e.g.):
- SunSpec - MQTT gateway
- PV surplus controlled charging of electric cars
- PV surplus water controlled water heating
- Peak shaving

Its main features are:
- Modbus support
- SunSpec device support (Solar inverters, smart meters)
- Shelly support (1 and 1PM)
- go-e Charger support
- MQTT export
- InfluxDB export
- Web app

## Requirements
- C++17
Expand Down Expand Up @@ -34,11 +51,18 @@ make
```

## Auto detection
ElsewhereEdge will scan your subnet with a mask of /24. E.g. if an instance is running on a host with IPv4 address 192.168.12.34, all IP addresses from 192.168.12.1 - 192.168.12.254 will be port scanned on port 502. If a valid SunSpec header is returned, the host will be considered as a valid SunSpec device.
Currently, there are some auto discoveries implemented.

### SunSpec
Iotic will scan your subnet with a mask of /24. E.g. if an instance is running on a host with IPv4 address 192.168.12.34, all IP addresses from 192.168.12.1 - 192.168.12.254 will be port scanned on port 502. If a valid SunSpec header is returned, the host will be considered as a valid SunSpec device.
This incoporates a small limitation, that currently there is only **one** Modbus device per host address supported, even though Modbus TCP would support multiple SunSpec devices per IP address.

### go-e Charger

### Shelly

## MQTT API
After being connected, the sunspec models will be polled and published via MQTT every 3 seconds.
After being connected, the discovered things will be polled and published via MQTT every 5 seconds.

The MQTT topic follows this scheme: `/elsewhere_<mac address>/<unique sunspec id>/<model id>/`.
Under this topic there are two sub-topics: `live` and `stats`. The *statistics* show the min/max values of the current day, as well as some other collected information. Thus, the *stats* topic will not be updated as often as the *live* topic, which gets an update every 3 seconds.
Expand All @@ -59,11 +83,15 @@ Currently, the following SunSpec models are supported: 101, 103, 160, 203. So, *
### Smart meters
- elgris SMART METER WiFi

### EV stations
- go-e Charger

### Smart plugs/relays
- Shelly 1 / 1PM

## Planned features
- config file
- debian package
- command line interface
- read battery inverters
- control wallboxes
- read shellies
- control shellies
4 changes: 2 additions & 2 deletions src/AppBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <things/ThingsRepository.h>
#include <things/http/HttpDiscovery.h>
#include <things/sunspec/SunSpecManager.h>
#include <websocket/WebSocketExporter.h>
#include <webserver/WebServer.h>

#include "Statistics.h"

Expand All @@ -29,5 +29,5 @@ class AppBackend : public QObject {
sunspec::SunSpecManager _sunSpecManager;
MqttExporter _mqttExporter;
std::optional<InfluxExporter> _influxExporter;
WebSocketExporter _webSocketExporter;
WebServer _webSocketExporter;
};
7 changes: 7 additions & 0 deletions src/things/ThingsRepository.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ dynamic_observable<std::list<ThingPtr>> ThingsRepository::things() const {
rpp::dynamic_observable<ThingPtr> ThingsRepository::thingAdded() const {
return _thingAdded.get_observable();
}

void ThingsRepository::setThingProperty(const std::string& id, WriteableThingProperty property, double value) const {
auto thing = thingById(id);
if (thing) {
thing->setProperty(property, value);
}
}
2 changes: 2 additions & 0 deletions src/things/ThingsRepository.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class ThingsRepository {
// Let's see, if that helps.
dynamic_observable<ThingPtr> thingAdded() const;

void setThingProperty(const std::string& id, WriteableThingProperty property, double value) const;

private:
std::list<ThingPtr> _things;
publish_subject<std::list<ThingPtr>> _thingsSubject;
Expand Down
14 changes: 10 additions & 4 deletions src/webapp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ if(NOT FLUTTER)
message(FATAL_ERROR "Flutter not found!")
endif()

execute_process(
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND flutter build web --web-renderer html --no-tree-shake-icons
)
#execute_process(
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
# COMMAND flutter build web --web-renderer html --no-tree-shake-icons
#)

file(GLOB_RECURSE webapp_src
"build/web/*.bin"
Expand All @@ -31,3 +31,9 @@ file(GLOB_RECURSE webapp_src
cmrc_add_resource_library(webapp WHENCE build/web
${webapp_src}
)

add_custom_command(TARGET webapp
PRE_BUILD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND flutter build web --web-renderer html --no-tree-shake-icons
)
10 changes: 10 additions & 0 deletions src/webapp/lib/data/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import 'package:iotic/data/site_live_data.dart';
import 'package:iotic/data/thing_live_data.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

enum WritableThingProperty { powerControl }

class Repository {
final siteLiveData = SiteLiveData(0, 0, 0).obs;
final things = <String, ThingLiveData>{}.obs;

void set(String id, WritableThingProperty property, dynamic value) {
var json = jsonEncode({
id.toString(): {property.name: value}
});
_channel.sink.add(json);
}

//final _port = (int.parse(html.window.location.port) + 1);
final _port = 7091;

Expand Down
2 changes: 1 addition & 1 deletion src/webapp/lib/ui/pages/site.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class Site extends StatelessWidget {
),
*/
barWidth: 2,
isCurved: true,
//isCurved: true,
preventCurveOverShooting: true,
shadow: const Shadow(
blurRadius: 8,
Expand Down
73 changes: 21 additions & 52 deletions src/webapp/lib/ui/things/thing_card.dart
Original file line number Diff line number Diff line change
@@ -1,70 +1,39 @@
import 'package:flutter/material.dart';
import 'package:iotic/ui/pages/value_unit.dart';
import 'package:iotic/ui/things/thing_property.dart';
import '../../data/thing_live_data.dart';
import 'package:get/get.dart';
import 'package:iotic/ui/things/thing_card_controller.dart';
import '../../data/repository.dart';

class ThingCard extends StatelessWidget {
ThingCard(this._name, this._properties, {super.key}) {
_name = _properties[ReadableThingProperty.name] ?? _name;
var type = _properties[ReadableThingProperty.type];
_icon = _typeToIcon[type] ?? Icons.device_hub;
if (_properties.containsKey(ReadableThingProperty.powerControl)) {
_hasPowerControl = true;
_powerControl = _properties[ReadableThingProperty.powerControl] != 0;
if (_icon == Icons.device_hub) {
_icon = Icons.electrical_services;
}
}
late final _control = Get.put(ThingCardController(_id), tag: _id);

dynamic p;
if ((p = _properties[ReadableThingProperty.power]) != null) {
_propertyWidgets
.add(ThingProperty(Icons.electric_bolt, p, powerUnit(p.round())));
}
if ((p = _properties[ReadableThingProperty.temperature]) != null) {
_propertyWidgets.add(ThingProperty(Icons.thermostat, p, '°C'));
}
}
ThingCard(this._id, {super.key});

final String _id;

String _name;
Map<ReadableThingProperty, dynamic> _properties;
List<ThingProperty> _propertyWidgets = List.empty(growable: true);
IconData _icon = Icons.device_hub;
bool _hasPowerControl = false;
bool _powerControl = false;
final _repo = Get.find<Repository>();

@override
Widget build(BuildContext context) {
return Column(children: [
Card(
child: ListTile(
leading: Icon(_icon, size: 32),
title: Text(_name),
subtitle: _propertyWidgets.isNotEmpty
? Row(
//mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _propertyWidgets)
child: Obx(
() => ListTile(
leading: Icon(_control.icon.value, size: 32),
title: Text(_control.name.value),
subtitle: _control.propertyWidgets.isNotEmpty
? Row(children: _control.propertyWidgets.values.toList())
: null,
trailing: _hasPowerControl
trailing: _control.hasPowerControl.value
? Switch(
activeColor: Colors.yellow,
value: _powerControl,
onChanged: (value) {},
value: _control.powerControl.value,
onChanged: (value) {
_repo.set(_id, WritableThingProperty.powerControl,
value ? 1.0 : 0.0);
},
)
: null),
)
))
]);
}

String powerUnit(int v) {
if (v.abs() < 1000) return "W";
return "kW";
}

static const Map<String?, IconData> _typeToIcon = {
"smartMeter": Icons.electric_meter,
"solarInverter": Icons.solar_power,
"evStation": Icons.ev_station,
null: Icons.device_hub
};
}
83 changes: 83 additions & 0 deletions src/webapp/lib/ui/things/thing_card_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:get/get_core/get_core.dart';
import 'package:get/get_instance/get_instance.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
import 'package:iotic/ui/things/thing_property.dart';

import '../../data/repository.dart';
import '../../data/thing_live_data.dart';

class ThingCardController extends GetxController {
ThingCardController(this._id);

final String _id;

static final Map<String?, IconData> _typeToIcon = {
"smartMeter": Icons.electric_meter,
"solarInverter": Icons.solar_power,
"evStation": Icons.ev_station,
null: Icons.device_hub
};

static String _powerUnit(int v) {
if (v.abs() < 1000) return "W";
return "kW";
}

final properties = <ReadableThingProperty, dynamic>{}.obs;
final name = "".obs;
final icon = Icons.device_hub.obs;
final hasPowerControl = false.obs;
final powerControl = false.obs;

final propertyWidgets = <ReadableThingProperty, ThingProperty>{}.obs;

@override
void onReady() {
//assignProperties(_repo.things);
_repo.things.listen((p0) {
assignProperties(p0);
});

super.onReady();
}

@override
void onClose() {
_repo.things.close();
super.onClose();
}

void assignProperties(Map<String, ThingLiveData> props) {
var p0 = props[_id]?.properties;
if (p0 != null) {
properties.value = p0;

name.value = p0[ReadableThingProperty.name] ?? _id;
icon.value =
_typeToIcon[p0[ReadableThingProperty.type]] ?? Icons.device_hub;

hasPowerControl.value =
p0.containsKey(ReadableThingProperty.powerControl);
if (hasPowerControl.value) {
powerControl.value = p0[ReadableThingProperty.powerControl] != 0;
if (icon.value == Icons.device_hub) {
icon.value = Icons.electrical_services;
}
}

dynamic p;
if ((p = p0[ReadableThingProperty.power]) != null) {
propertyWidgets[ReadableThingProperty.power] =
ThingProperty(Icons.electric_bolt, p, _powerUnit(p.round()));
}
if ((p = p0[ReadableThingProperty.temperature]) != null) {
propertyWidgets[ReadableThingProperty.temperature] =
ThingProperty(Icons.thermostat, p, '°C');
}
}
}

final Repository _repo = Get.find<Repository>();
}
24 changes: 0 additions & 24 deletions src/webapp/lib/ui/things/things_controller.dart

This file was deleted.

Loading

0 comments on commit 639fe9d

Please sign in to comment.