Skip to content

Commit

Permalink
Added MGW support
Browse files Browse the repository at this point in the history
  • Loading branch information
grimmpp committed May 22, 2024
1 parent bee2689 commit 4781d57
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 56 deletions.
3 changes: 3 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Change Log

## v0.1.22 Support for MGW Gateway
* Added support for [MGW Gateway](https://www.piotek.de/PioTek-MGW-POE) (ESP3 over LAN)

## v0.1.21 EEP representation bug fixes
* Ignores unknown devices
* Fixed EEP representation in dorp down
Expand Down
84 changes: 61 additions & 23 deletions eo_man/controller/serial_controller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from enum import Enum
import sys
import glob
Expand All @@ -9,18 +8,21 @@
import logging
import threading


import serial.tools.list_ports
from serial.tools.list_ports_common import ListPortInfo

from eltakobus import *
from eltakobus.device import *
from eltakobus.locking import buslocked, UNLOCKED
from eltakobus.message import Regular4BSMessage

from esp2_gateway_adapter.esp3_serial_com import ESP3SerialCommunicator
from esp2_gateway_adapter.esp3_tcp_com import TCP2SerialCommunicator, detect_lan_gateways

from ..data import data_helper
from ..data.device import Device
from ..data.const import GatewayDeviceType
from ..data.const import GatewayDeviceType as GDT, GATEWAY_DISPLAY_NAMES as GDN

from .app_bus import AppBusEventType, AppBus

Expand All @@ -44,18 +46,23 @@ def on_window_closed(self, data) -> None:
self.kill_serial_connection_before_exit()




def get_serial_ports(self, device_type:str, force_reload:bool=False) ->list[str]:
if force_reload or self.port_mapping is None:
self.port_mapping = self._get_gateway2serial_port_mapping()

if device_type == 'FAM14':
return self.port_mapping[GatewayDeviceType.GatewayEltakoFAM14.value]
elif device_type == 'FGW14-USB':
return self.port_mapping[GatewayDeviceType.GatewayEltakoFGW14USB.value]
elif device_type == 'FAM-USB':
return self.port_mapping[GatewayDeviceType.GatewayEltakoFAMUSB.value]
elif device_type == 'USB300':
return self.port_mapping[GatewayDeviceType.GatewayEnOceanUSB300.value]
self.port_mapping[GDT.LAN] = detect_lan_gateways()

if device_type == GDN[GDT.EltakoFAM14]:
return self.port_mapping[GDT.EltakoFAM14.value]
elif device_type == GDN[GDT.EltakoFGW14USB]:
return self.port_mapping[GDT.EltakoFGW14USB.value]
elif device_type == GDN[GDT.EltakoFAMUSB]:
return self.port_mapping[GDT.EltakoFAMUSB.value]
elif device_type == GDN[GDT.USB300]:
return self.port_mapping[GDT.USB300.value]
elif device_type == GDN[GDT.LAN]:
return self.port_mapping[GDT.LAN]
else:
return []

Expand Down Expand Up @@ -89,10 +96,10 @@ def _get_gateway2serial_port_mapping(self) -> dict[str:list[str]]:

# ports = [p.device for p in _ports if p.vid == self.USB_VENDOR_ID]

fam14 = GatewayDeviceType.GatewayEltakoFAM14.value
usb300 = GatewayDeviceType.GatewayEnOceanUSB300.value
famusb = GatewayDeviceType.GatewayEltakoFAMUSB.value
fgw14usb = GatewayDeviceType.GatewayEltakoFGW14USB.value
fam14 = GDT.EltakoFAM14.value
usb300 = GDT.USB300.value
famusb = GDT.EltakoFAMUSB.value
fgw14usb = GDT.EltakoFGW14USB.value
result = { fam14: [], usb300: [], famusb: [], fgw14usb: [], 'all': [] }

count = 0
Expand Down Expand Up @@ -183,14 +190,21 @@ def _received_serial_event(self, message) -> None:
def establish_serial_connection(self, serial_port:str, device_type:str) -> None:
baudrate:int=57600
delay_message:float=.1
if device_type == 'FAM-USB':
if device_type == GDN[GDT.EltakoFAMUSB]:
baudrate = 9600
elif device_type == 'FAM14':
elif device_type == GDN[GDT.EltakoFAM14]:
delay_message = 0.001

try:
if not self.is_serial_connection_active():
if device_type == 'USB300':
if device_type == GDN[GDT.LAN]:
baudrate=5100
self._serial_bus = TCP2SerialCommunicator(serial_port, 5100,
callback=self._received_serial_event,
esp2_translation_enabled=True,
auto_reconnect=False
)
elif device_type == GDN[GDT.ESP3]:
self._serial_bus = ESP3SerialCommunicator(serial_port,
callback=self._received_serial_event,
esp2_translation_enabled=True,
Expand All @@ -212,7 +226,7 @@ def establish_serial_connection(self, serial_port:str, device_type:str) -> None:
self.connected_gateway_type = device_type
self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"Serial connection established. serial port: {serial_port}, baudrate: {baudrate}", 'color':'green'})

if device_type == 'FAM14':
if device_type == GDN[GDT.EltakoFAM14]:

def run():
asyncio.run( self._get_fam14_device_on_bus() )
Expand All @@ -223,10 +237,12 @@ def run():
t = threading.Thread(target=run)
t.start()
else:
if device_type == 'FAM-USB':
if device_type == GDN[GDT.EltakoFAMUSB]:
asyncio.run( self.async_create_fam_usb_device() )
elif device_type == 'USB300':
elif device_type == GDN[GDT.USB300]:
asyncio.run( self.async_create_usb300_device() )
elif device_type == GDN[GDT.LAN]:
asyncio.run( self.async_create_lan_gw_device() )

self.app_bus.fire_event(
AppBusEventType.CONNECTION_STATUS_CHANGE,
Expand All @@ -241,14 +257,36 @@ def run():
logging.exception(msg, exc_info=True)


async def async_create_lan_gw_device(self):
try:
self._serial_bus.set_callback( None )

self.current_base_id = b2s(self._serial_bus.base_id)
self.gateway_id = self.current_base_id

await self.app_bus.async_fire_event(AppBusEventType.ASYNC_TRANSCEIVER_DETECTED, {'type': GDN[GDT.LAN],
'base_id': self.current_base_id,
'gateway_id': self.gateway_id,
'tcm_version': '',
'api_version': ''})


except Exception as e:
msg = 'Failed to get information about LAN GW (TCP2ESP3)!!!'
self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': msg, 'log-level': 'ERROR', 'color': 'red'})
logging.exception(msg, exc_info=True)
raise e
finally:
self._serial_bus.set_callback( self._received_serial_event )

async def async_create_usb300_device(self):
try:
self._serial_bus.set_callback( None )

self.current_base_id = b2s(self._serial_bus.base_id)
self.gateway_id = self.current_base_id

await self.app_bus.async_fire_event(AppBusEventType.ASYNC_TRANSCEIVER_DETECTED, {'type': 'USB300',
await self.app_bus.async_fire_event(AppBusEventType.ASYNC_TRANSCEIVER_DETECTED, {'type': GDN[GDT.USB300],
'base_id': self.current_base_id,
'gateway_id': self.gateway_id,
'tcm_version': '',
Expand Down Expand Up @@ -303,7 +341,7 @@ async def async_create_fam_usb_device(self):
tcm_sw_v = '.'.join(str(n) for n in response.body[2:6])
api_v = '.'.join(str(n) for n in response.body[6:10])

await self.app_bus.async_fire_event(AppBusEventType.ASYNC_TRANSCEIVER_DETECTED, {'type': 'FAM-USB',
await self.app_bus.async_fire_event(AppBusEventType.ASYNC_TRANSCEIVER_DETECTED, {'type': GDN[GDT.EltakoFAMUSB],
'base_id': self.current_base_id,
'gateway_id': self.gateway_id,
'tcm_version': tcm_sw_v,
Expand Down
44 changes: 32 additions & 12 deletions eo_man/data/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
CONF_INVERT_SIGNAL: Final = "invert_signal"
CONF_VOC_TYPE_INDEXES: Final = "voc_type_indexes"


class LANGUAGE_ABBREVIATION(StrEnum):
LANG_ENGLISH = 'en'
LANG_GERMAN = 'de'
Expand All @@ -79,10 +80,12 @@ class LANGUAGE_ABBREVIATION(StrEnum):
]

class GatewayDeviceType(str, Enum):
GatewayEltakoFAM14 = 'fam14'
GatewayEltakoFGW14USB = 'fgw14usb'
GatewayEltakoFAMUSB = 'fam-usb' # ESP2 transceiver: https://www.eltako.com/en/product/professional-standard-en/three-phase-energy-meters-and-one-phase-energy-meters/fam-usb/
GatewayEnOceanUSB300 = 'enocean-usb300' # not yet supported
EltakoFAM14 = 'fam14'
EltakoFGW14USB = 'fgw14usb'
EltakoFAMUSB = 'fam-usb' # ESP2 transceiver: https://www.eltako.com/en/product/professional-standard-en/three-phase-energy-meters-and-one-phase-energy-meters/fam-usb/
USB300 = 'enocean-usb300'
ESP3 = 'esp3-gateway'
LAN = 'mgw-lan'

@classmethod
def find(cls, value):
Expand All @@ -93,19 +96,36 @@ def find(cls, value):

@classmethod
def is_transceiver(cls, dev_type) -> bool:
return dev_type in [GatewayDeviceType.GatewayEltakoFAMUSB, GatewayDeviceType.EnOceanUSB300]
return dev_type in [GatewayDeviceType.EltakoFAMUSB, GatewayDeviceType.USB300]

@classmethod
def is_bus_gateway(cls, dev_type) -> bool:
return dev_type in [GatewayDeviceType.GatewayEltakoFAM14, GatewayDeviceType.GatewayEltakoFGW14USB]
return dev_type in [GatewayDeviceType.EltakoFAM14, GatewayDeviceType.EltakoFGW14USB]

@classmethod
def is_esp2_gateway(cls, dev_type) -> bool:
return dev_type in [GatewayDeviceType.GatewayEltakoFAM14, GatewayDeviceType.GatewayEltakoFGW14USB, GatewayDeviceType.GatewayEltakoFAMUSB]
return dev_type in [GatewayDeviceType.EltakoFAM14, GatewayDeviceType.EltakoFGW14USB, GatewayDeviceType.EltakoFAMUSB]

BAUD_RATE_DEVICE_TYPE_MAPPING: dict = {
GatewayDeviceType.GatewayEltakoFAM14: 57600,
GatewayDeviceType.GatewayEltakoFGW14USB: 57600,
GatewayDeviceType.GatewayEltakoFAMUSB: 9600,
GatewayDeviceType.GatewayEnOceanUSB300: 57600,
}
GatewayDeviceType.EltakoFAM14: 57600,
GatewayDeviceType.EltakoFGW14USB: 57600,
GatewayDeviceType.EltakoFAMUSB: 9600,
GatewayDeviceType.USB300: 57600,
GatewayDeviceType.ESP3: 57600,
}

GATEWAY_DISPLAY_NAMES = {
GatewayDeviceType.EltakoFAM14: "FAM14 (ESP2)",
GatewayDeviceType.EltakoFGW14USB: 'FGW14-USB (ESP2)',
GatewayDeviceType.EltakoFAMUSB: 'FAM-USB (ESP2)',
GatewayDeviceType.USB300: 'ESP3 Gateway',
GatewayDeviceType.ESP3: 'ESP3 Gateway',
GatewayDeviceType.LAN: 'MGW LAN/Wifi',
}

def get_display_names():
result = []
for v in GATEWAY_DISPLAY_NAMES.values():
if v not in result:
result.append(v)
return result
12 changes: 12 additions & 0 deletions eo_man/data/data_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
{'hw-type': 'FSB14', CONF_EEP: 'G5-3F-7F', 'sender_eep': 'H5-3F-7F', CONF_TYPE: Platform.COVER, 'PCT14-function-group': 2, 'PCT14-key-function': 31, 'description': 'Eltako cover', 'address_count': 2},

{'hw-type': 'FAE14SSR', CONF_EEP: 'A5-10-06', 'sender_eep': 'A5-10-06', CONF_TYPE: Platform.CLIMATE, 'PCT14-function-group': 3, 'PCT14-key-function': 65, 'description': 'Eltako heating/cooling', 'address_count': 2},

{'hw-type': 'FSR61NP', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'description': 'Eltako relay', 'address_count': 1},
{'hw-type': 'FSR61/8-24V UC', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'description': 'Eltako relay', 'address_count': 1},
{'hw-type': 'FSR61-230V', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'description': 'Eltako relay', 'address_count': 1},
{'hw-type': 'FSR61G-230V', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'description': 'Eltako relay', 'address_count': 1},
{'hw-type': 'FSR61LN-230V', CONF_EEP: 'M5-38-08', 'sender_eep': 'A5-38-08', CONF_TYPE: Platform.LIGHT, 'description': 'Eltako relay', 'address_count': 1},

{'hw-type': 'FSB61-230V', CONF_EEP: 'G5-3F-7F', 'sender_eep': 'H5-3F-7F', CONF_TYPE: Platform.COVER, 'address_count': 1},
{'hw-type': 'FSB61NP-230V', CONF_EEP: 'G5-3F-7F', 'sender_eep': 'H5-3F-7F', CONF_TYPE: Platform.COVER, 'address_count': 1},
]

ORG_MAPPING = {
Expand Down Expand Up @@ -96,6 +105,9 @@ def find_device_info_by_device_type(device_type:str) -> dict:
return i
return None

def get_known_device_types() -> list:
return sorted(list(set([t['hw-type'] for t in EEP_MAPPING])))

def get_eep_from_key_function_name(kf: KeyFunction) -> str:
pos = KeyFunction(kf).name.find('EEP_')
if pos > -1:
Expand Down
17 changes: 16 additions & 1 deletion eo_man/data/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ async def async_get_bus_device_by_natvice_bus_object(cls, device: BusObject, fam

return bd


@classmethod
def init_sender_fields(cls, device, overwrite:bool=False):
info:dict = find_device_info_by_device_type(device.device_type)

if device.additional_fields is None:
device.additional_fields = {}

if info.get('sender_eep', None):
if overwrite or not ('sender' in device.additional_fields and len(device.additional_fields['sender']) > 0):
device.additional_fields['sender'] = {
CONF_ID: a2s( a2i(device.address) % 128 )[-2:],
CONF_EEP: info.get('sender_eep')
}

@classmethod
def set_suggest_ha_config(cls, device):
id = int.from_bytes( AddressExpression.parse(device.address)[0], 'big')
Expand All @@ -157,7 +172,7 @@ def set_suggest_ha_config(cls, device):

if info.get('sender_eep', None):
device.additional_fields['sender'] = {
CONF_ID: a2s( SENDER_BASE_ID + id ),
CONF_ID: a2s( a2i(device.address) % 128 )[-2:],
CONF_EEP: info.get('sender_eep')
}

Expand Down
12 changes: 6 additions & 6 deletions eo_man/data/ha_config_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ def get_description(self) -> str:
def get_gateway_by(self, gw_d:Device) -> GatewayDeviceType:
gw_type = None
if gw_d.is_fam14():
gw_type = GatewayDeviceType.GatewayEltakoFAM14.value
gw_type = GatewayDeviceType.EltakoFAM14.value
elif gw_d.is_fgw14_usb():
gw_type = GatewayDeviceType.GatewayEltakoFGW14USB.value
gw_type = GatewayDeviceType.EltakoFGW14USB.value
elif gw_d.is_fam_usb():
gw_type = GatewayDeviceType.GatewayEltakoFAMUSB.value
gw_type = GatewayDeviceType.EltakoFAMUSB.value
elif gw_d.is_usb300():
gw_type = GatewayDeviceType.GatewayEnOceanUSB300.value
gw_type = GatewayDeviceType.USB300.value
return gw_type

def generate_ha_config(self, device_list:list[Device]) -> str:
Expand All @@ -63,8 +63,8 @@ def generate_ha_config(self, device_list:list[Device]) -> str:
out += f" {CONF_GATEWAY}:\n"
out += f" - {CONF_ID}: {global_gw_id}\n"

gw_fam14 = GatewayDeviceType.GatewayEltakoFAM14.value
gw_fgw14usb = GatewayDeviceType.GatewayEltakoFGW14USB.value
gw_fam14 = GatewayDeviceType.EltakoFAM14.value
gw_fgw14usb = GatewayDeviceType.EltakoFGW14USB.value

gw_type = self.get_gateway_by(gw_d)
out += f" {CONF_DEVICE_TYPE}: {gw_type} # you can simply change {gw_fam14} to {gw_fgw14usb}\n"
Expand Down
Loading

0 comments on commit 4781d57

Please sign in to comment.