Put dependencies in this repository and rename
authorBenjamin Braatz <bb@bbraatz.eu>
Fri, 22 Jan 2021 09:26:47 +0000 (10:26 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Fri, 22 Jan 2021 09:26:47 +0000 (10:26 +0100)
25 files changed:
conf.json
controlpi.service [deleted file]
gevent/__init__.py [new file with mode: 0644]
gevent/interface.py [new file with mode: 0644]
gevent/mixin.py [new file with mode: 0644]
gmodbus/__init__.py [new file with mode: 0644]
gmodbus/hitachi.py [new file with mode: 0644]
gmodbus/transport.py [new file with mode: 0644]
gpin/__init__.py [new file with mode: 0644]
gpin/composition.py [new file with mode: 0644]
gpin/gpio.py [new file with mode: 0644]
gpin/interface.py [new file with mode: 0644]
gpin/pcf8574.py [new file with mode: 0644]
gpin/pigpio.py [new file with mode: 0644]
graphit_controlpi/__init__.py [deleted file]
graphit_controlpi/config.py [deleted file]
graphit_controlpi/main.py [deleted file]
graphit_controlpi/websocket.py [deleted file]
modbus_test.py [new file with mode: 0644]
schaltschrank.service [new file with mode: 0644]
schaltschrank/__init__.py [new file with mode: 0644]
schaltschrank/config.py [new file with mode: 0644]
schaltschrank/main.py [new file with mode: 0644]
schaltschrank/websocket.py [new file with mode: 0644]
setup.py

index f3d175770a9c92ee73e1ee69bd275f64f75dfe4b..ad32949bea728f4e2618b6e7688d754099236384 100644 (file)
--- a/conf.json
+++ b/conf.json
@@ -52,5 +52,5 @@
         [ "E4-5", "T2-13" ], [ "E4-6", "T2-14" ],
         [ "E4-7", "T2-15" ], [ "E4-8", "T2-16" ] ] } ],
   "modbus":
-  { "serial device": "/dev/serial1",
+  { "serial device": "/dev/serial0",
     "slave id": 1 } }
diff --git a/controlpi.service b/controlpi.service
deleted file mode 100644 (file)
index 535996c..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Control Pi Service
-Wants=network-online.target
-After=network-online.target
-
-[Service]
-WorkingDirectory=/home/pi
-Environment=PYTHONUNBUFFERED=1
-ExecStart=/home/pi/controlpi/bin/python -m graphit_controlpi.main conf.json web/
-
-[Install]
-WantedBy=multi-user.target
diff --git a/gevent/__init__.py b/gevent/__init__.py
new file mode 100644 (file)
index 0000000..d2148e1
--- /dev/null
@@ -0,0 +1,4 @@
+__all__ = ['EventEmitterInterface', 'EventEmitterMixin']
+
+from .interface import EventEmitterInterface
+from .mixin import EventEmitterMixin
diff --git a/gevent/interface.py b/gevent/interface.py
new file mode 100644 (file)
index 0000000..939f1e4
--- /dev/null
@@ -0,0 +1,25 @@
+import abc
+from typing import Hashable, Callable
+
+
+class EventEmitterInterface(abc.ABC):
+    @abc.abstractmethod
+    def on(self, event: Hashable, callback: Callable) -> Hashable:
+        '''
+        Registers the given callback for the given event.
+        Returns handle to unregister the given callback.
+        '''
+
+    @abc.abstractmethod
+    def off(self, handle: Hashable) -> bool:
+        '''
+        Unregisters a previously registered callback by the given handle.
+        Returns True on success.
+        '''
+
+    @abc.abstractmethod
+    def _emit(self, event: Hashable, *args, **kwargs) -> None:
+        '''
+        Emits the given event by calling all callbacks registered for this
+        event.
+        '''
diff --git a/gevent/mixin.py b/gevent/mixin.py
new file mode 100644 (file)
index 0000000..dfc618b
--- /dev/null
@@ -0,0 +1,64 @@
+import uuid
+from typing import Hashable, Callable, MutableMapping, Mapping
+
+from .interface import EventEmitterInterface
+
+EvMap = MutableMapping[Hashable, Hashable]
+CbMap = MutableMapping[Hashable, MutableMapping[Hashable, Callable]]
+
+
+class EventEmitterMixin(EventEmitterInterface):
+    def on(self, event: Hashable, callback: Callable) -> Hashable:
+        events: EvMap
+        callbacks: CbMap
+        try:
+            events = self._eventEmitterMixinEvents
+            callbacks = self._eventEmitterMixinCallbacks
+        except AttributeError:
+            self._eventEmitterMixinEvents: EvMap = {}
+            self._eventEmitterMixinCallbacks: CbMap = {}
+            events = self._eventEmitterMixinEvents
+            callbacks = self._eventEmitterMixinCallbacks
+
+        if event not in callbacks:
+            callbacks[event] = {}
+
+        handle = uuid.uuid4()
+        while handle in events:
+            handle = uuid.uuid4()
+
+        events[handle] = event
+        callbacks[event][handle] = callback
+
+        return handle
+
+    def off(self, handle: Hashable) -> bool:
+        try:
+            events = self._eventEmitterMixinEvents
+            callbacks = self._eventEmitterMixinCallbacks
+        except AttributeError:
+            return False
+
+        if handle not in events:
+            return False
+        event = events[handle]
+
+        del events[handle]
+        del callbacks[event][handle]
+
+        if not callbacks[event]:
+            del callbacks[event]
+
+        return True
+
+    def _emit(self, event: Hashable, *args, **kwargs) -> None:
+        try:
+            callbacks = self._eventEmitterMixinCallbacks
+        except AttributeError:
+            return
+
+        if event not in callbacks:
+            return
+
+        for callback in callbacks[event].values():
+            callback(*args, **kwargs)
diff --git a/gmodbus/__init__.py b/gmodbus/__init__.py
new file mode 100644 (file)
index 0000000..46a4c07
--- /dev/null
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+
+__all__ = ('DatatypesProtocol')
+
+
+import random
+import struct
+
+from typing import Mapping
+
+from .transport import ClientInterface
+
+
+class DatatypesProtocol():
+
+  def __init__(self, client: ClientInterface, coils, registers) -> None:
+    self.__client = client
+    self.__coils = coils
+    self.__registers = registers
+
+  async def read_coil(self, coil: int) -> bool:
+    assert coil in self.__coils, 'unknown coil'
+
+    res = await self.__client.read_coils(coil - 1, 1)
+    return res[0]
+
+  async def write_coil(self, coil: int, value: bool) -> None:
+    assert coil in self.__coils, 'unknown coil'
+
+    await self.__client.write_single_coil(coil - 1, value)
+
+  async def read_register(self, register: int) -> float:
+    assert register in self.__registers, 'unknown register'
+
+    rw = self.__registers[register]['rw']
+    assert rw is 'r' or rw is 'rw', 'register is not readable'
+
+    rType = self.__registers[register]['type']
+    rConf = self.__registers[register]['conf']
+    if rType == 'uint16':
+      return await self.__read_uint16_register(register, **rConf)
+    elif rType == 'uint32':
+      return await self.__read_uint32_register(register, **rConf)
+    elif rType == 'int32':
+      return await self.__read_int32_register(register, **rConf)
+    elif rType == 'enum':
+      return await self.__read_enum_register(register, **rConf)
+    else:
+      assert False, 'unknown register type'
+
+
+  async def write_register(self, register: int, value: int) -> None:
+    assert register in self.__registers, 'unknown register'
+
+    rw = self.__registers[register]['rw']
+    assert rw is 'w' or rw is 'rw', 'register is not writable'
+
+    rType = self.__registers[register]['type']
+    rConf = self.__registers[register]['conf']
+    if rType == 'uint16':
+      await self.__write_uint16_register(register, value, **rConf)
+    elif rType == 'uint32':
+      await self.__write_uint32_register(register, value, **rConf)
+    elif rType == 'int32':
+      await self.__write_int32_register(register, value, **rConf)
+    elif rType == 'enum':
+      await self.__write_enum_register(register, value, ** rConf)
+    else:
+      assert False, 'unknown register type'
+
+  async def __read_uint16_register(self, register: int, min: int, max: int, unit: str, scale: int)-> float:
+    res = await self.__client.read_holding_registers(register-1, 1)
+    res = res[0] / (1/scale)
+
+    assert res >= min and res <= max
+    return res
+
+  async def __write_uint16_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None:
+    assert value >= min and value <= max
+
+    await self.__client.write_multiple_registers(register-1, [int(value * (1/scale))])
+
+  async def __read_uint32_register(self, register: int, min: int, max: int, unit: str, scale: int) -> float:
+    res = await self.__client.read_holding_registers(register-1, 2)
+    res = struct.pack('>HH', res[0], res[1])
+    res = struct.unpack('>L', res)
+    res = res[0] / (1/scale)
+
+    assert res >= min and res <= max
+    return res
+
+  async def __write_uint32_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None:
+    assert value >= min and value <= max
+
+    req = int(value * (1/scale))
+    req = struct.pack('>L', req)
+    req = struct.unpack('>HH', req)
+
+    await self.__client.write_multiple_registers(register-1, [x for x in req])
+
+  async def __read_int32_register(self, register: int, min: int, max: int, unit: str, scale: int) -> float:
+    res = await self.__client.read_holding_registers(register-1, 2)
+
+    res = struct.pack('>HH', res[0], res[1])
+    res = struct.unpack('>l', res)
+    res = res[0] / (1/scale)
+
+    assert res >= min and res <= max
+    return res
+
+  async def __write_int32_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None:
+    assert value >= min and value <= max
+
+    req = int(value * (1/scale))
+    req = struct.pack('>l', req)
+    req = struct.unpack('>HH', req)
+
+    await self.__client.write_multiple_registers(register-1, [x for x in req])
+
+  async def __read_enum_register(self, register: int, values: Mapping[int, str]) -> int:
+    res = await self.__client.read_holding_registers(register - 1, 1)
+    res = res[0]
+
+    assert res in values
+    return res
+
+  async def __write_enum_register(self, register: int, value: int, values: Mapping[int, str]) -> None:
+    assert value in values
+
+    await self.__client.write_multiple_registers(register-1, [value])
+
+  async def loopback_test(self, testvalue: int = None) -> None:
+    if testvalue is not None:
+      assert testvalue >= 0 and testvalue < 2 ^ 16
+    else:
+      testvalue = random.randrange(2 ^ 16)
+
+    await self.__client.loopback_test(testvalue)
diff --git a/gmodbus/hitachi.py b/gmodbus/hitachi.py
new file mode 100644 (file)
index 0000000..2fb7372
--- /dev/null
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+
+__all__ = ('SJP1Fu')
+
+
+from . import DatatypesProtocol, ClientInterface
+
+
+class SJP1Fu():
+
+  def __init__(self, client: ClientInterface) -> None:
+    self.__protocol = DatatypesProtocol(client, SJP1FU_COILS, SJP1FU_REGISTERS)
+
+  async def set_frequency(self, frequency: int) -> None:
+    # Page 532 / 14-41
+    await self.__protocol.write_register(10502, frequency)
+
+  async def get_frequency(self) -> int:
+    # Page 532 / 14-41
+    return int(await self.__protocol.read_register(10502))
+
+  async def start_inverter(self) -> None:
+    # Page 518 / 14-27
+    await self.__protocol.write_coil(1, True)
+
+  async def stop_inverter(self) -> None:
+    # Page 518 / 14-27
+    await self.__protocol.write_coil(1, False)
+
+  @property
+  async def inverter_active(self) -> bool:
+    return await self.__protocol.read_coil(1)
+
+
+SJP1FU_COILS = {
+  1: {
+    'name': 'Operation Command',
+    'rw': 'rw',
+    'values': [ 'Stop', 'Run' ]
+  }
+}
+
+
+SJP1FU_REGISTERS = {
+  10001: {
+    'code': 'dA-01',
+    'name': 'Output frequency monitor',
+    'rw': 'r',
+    'type': 'uint16',
+    'conf': {
+      'min': 0,
+      'max': 590,
+      'unit': 'Hz',
+      'scale': 0.01,
+    },
+  },
+
+  10003: {
+    'code': 'dA-03',
+    'name': 'Operation direction monitor',
+    'rw': 'r',
+    'type': 'enum',
+    'conf': {
+      'values': {
+        0: 'Stop',
+        1: 'Zero-speed out',
+        2: 'Forward run',
+        3: 'Reverse run',
+      }
+    }
+  },
+
+  10502: {
+    'name': 'Set Frequency',
+    'rw': 'rw',
+    'type': 'int32',
+    'conf': {
+      'min': -590,
+      'max': 590,
+      'unit': 'Hz',
+      'scale': 0.01,
+    },
+  },
+
+  11010: {
+    'code': 'FA-10',
+    'name': 'Acceleration time (monitor + setting)',
+    'rw': 'rw',
+    'type': 'uint32',
+    'conf': {
+      'min': 0,
+      'max': 3600,
+      'unit': 's',
+      'scale': 0.01,
+    },
+  },
+  11012: {
+    'code': 'FA-12',
+    'name': 'Deceleration time (monitor + setting)',
+    'rw': 'rw',
+    'type': 'uint32',
+    'conf': {
+      'min': 0,
+      'max': 3600,
+      'unit': 's',
+      'scale': 0.01,
+    },
+  },
+
+  12501: {
+    'code': 'AF101',
+    'name': 'First DC braking selection',
+    'rw': 'rw',
+    'type': 'enum',
+    'conf': {
+      'values': {
+#        0: 'Stop',
+#        1: 'Zero-speed out',
+#        2: 'Forward run',
+#        3: 'Reverse run',
+      }
+    }
+  },
+}
diff --git a/gmodbus/transport.py b/gmodbus/transport.py
new file mode 100644 (file)
index 0000000..8959145
--- /dev/null
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+__all__ = ('ClientInterface', 'SerialPort', 'SerialClient')
+
+
+import os
+import abc
+import fcntl
+import struct
+import asyncio
+
+from typing import List
+
+from umodbus.client.serial import rtu
+from umodbus.functions import expected_response_pdu_size_from_request_pdu
+from umodbus.client.serial.redundancy_check import add_crc
+
+
+class ClientInterface():
+
+  @abc.abstractmethod
+  async def read_coils(self, starting_address: int, quantity: int) -> List[bool]:
+    ...
+
+  @abc.abstractmethod
+  async def read_discrete_inputs(self, starting_address: int, quantity: int) -> List[bool]:
+    ...
+
+  @abc.abstractmethod
+  async def read_holding_registers(self, starting_address: int, quantity: int) -> List[int]:
+    ...
+
+  @abc.abstractmethod
+  async def read_input_registers(self, starting_address: int, quantity: int) -> List[int]:
+    ...
+
+  @abc.abstractmethod
+  async def write_single_coil(self, address: int, value: bool) -> None:
+    ...
+
+  @abc.abstractmethod
+  async def write_single_register(self, address: int, value: int) -> None:
+    ...
+
+  @abc.abstractmethod
+  async def write_multiple_coils(self, starting_address: int, values: List[bool]) -> None:
+    ...
+
+  @abc.abstractmethod
+  async def write_multiple_registers(self, starting_address: int, values: List[int]) -> None:
+    ...
+
+  @abc.abstractmethod
+  async def loopback_test(self, value: int) -> int:
+    ...
+
+
+class SerialPort():
+
+  def __init__(self, filename: str) -> None:
+    self.__file = os.open(filename, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
+
+    self.__read_lock = asyncio.Lock()
+    self.__write_lock = asyncio.Lock()
+
+    fcntl.fcntl(self.__file, fcntl.F_SETFL, os.O_NONBLOCK)
+
+  async def __read(self, size: int = 1) -> bytes:
+    assert size > 0
+
+    loop = asyncio.get_event_loop()
+    future = loop.create_future()
+
+    loop.add_reader(self.__file, lambda: future.set_result(os.read(self.__file, size)))
+    future.add_done_callback(lambda _f: loop.remove_reader(self.__file))
+
+    return await future
+
+  async def read(self, size: int = 1) -> bytes:
+    assert size > 0
+
+    async with self.__read_lock:
+      buffer = b""
+      while size > 0:
+        data = await self.__read(size)
+        if data is b"":
+          return buffer
+
+        buffer += data
+        size -= len(data)
+
+      return buffer
+
+  async def __write(self, data: bytes) -> int:
+    assert len(data) > 0
+
+    loop = asyncio.get_event_loop()
+    future = loop.create_future()
+
+    loop.add_writer(self.__file, lambda: future.set_result(os.write(self.__file, data)))
+    future.add_done_callback(lambda _f: loop.remove_writer(self.__file))
+
+    return await future
+
+  async def write(self, data: bytes) -> int:
+    assert len(data) > 0
+
+    async with self.__write_lock:
+      size = 0
+      while len(data) > 0:
+        written = await self.__write(data)
+        if written is 0:
+          return size
+
+        data = data[written:]
+        size += written
+
+      return size
+
+
+class SerialClient(ClientInterface):
+
+  def __init__(self, port: SerialPort, slave_id: int) -> None:
+    self.__port = port
+    self.__slave_id = slave_id
+
+  async def read_coils(self, starting_address: int, quantity: int) -> List[bool]:
+    response = await self.__send_message(rtu.read_coils(self.__slave_id, starting_address, quantity))
+    response = [bool(x) for x in response]
+
+    return response
+
+  async def read_discrete_inputs(self, starting_address: int, quantity: int) -> List[bool]:
+    response = await self.__send_message(rtu.read_discrete_inputs(self.__slave_id, starting_address, quantity))
+    response = [bool(x) for x in response]
+
+    return response
+
+  async def read_holding_registers(self, starting_address: int, quantity: int) -> List[int]:
+    return await self.__send_message(rtu.read_holding_registers(self.__slave_id, starting_address, quantity))
+
+  async def read_input_registers(self, starting_address: int, quantity: int) -> List[int]:
+    return await self.__send_message(rtu.read_input_registers(self.__slave_id, starting_address, quantity))
+
+  async def write_single_coil(self, address: int, value: bool) -> None:
+    await self.__send_message(rtu.write_single_coil(self.__slave_id, address, int(value)))
+
+  async def write_single_register(self, address: int, value: int) -> None:
+    await self.__send_message(rtu.write_single_register(self.__slave_id, address, value))
+
+  async def write_multiple_coils(self, starting_address: int, values: List[bool]) -> None:
+    await self.__send_message(rtu.write_multiple_coils(self.__slave_id, starting_address, [int(x) for x in values]))
+
+  async def write_multiple_registers(self, starting_address: int, values: List[int]) -> None:
+    await self.__send_message(rtu.write_multiple_registers(self.__slave_id, starting_address, values))
+
+  async def loopback_test(self, value: int) -> int:
+    assert 0 <= value < 2 ^ 16
+
+    req = struct.pack('>BBHH', 1, 8, 0, value)
+    req = add_crc(req)
+
+    await self.__port.write(req)
+
+    res = await self.__port.read(len(req))
+    assert req == res
+
+    return value
+
+  async def __send_message(self, message: bytes) -> List[int]:
+    await self.__port.write(message)
+
+    # Check exception ADU (which is shorter than all other responses) first.
+    exception_adu_size = 5
+    response_error_adu = await self.__port.read(exception_adu_size)
+    rtu.raise_for_exception_adu(response_error_adu)
+
+    expected_response_size = \
+        expected_response_pdu_size_from_request_pdu(message[1:-2]) + 3
+    response_remainder = await self.__port.read(expected_response_size - exception_adu_size)
+
+    if len(response_remainder) < expected_response_size - exception_adu_size:
+      raise ValueError
+
+    result = rtu.parse_response_adu(response_error_adu + response_remainder, message)
+
+    if not isinstance(result, list):
+      return [result]
+
+    return result
diff --git a/gpin/__init__.py b/gpin/__init__.py
new file mode 100644 (file)
index 0000000..34d0b74
--- /dev/null
@@ -0,0 +1,13 @@
+__all__ = ['PinInterface',
+           'InvertingPin', 'SwitchPin', 'GuardedPin', 'TimerPin',
+           'AggregatePinInterface', 'AbstractAggregatePin',
+           'OrAggregatePin', 'AndAggregatePin',
+           'GPIOInputPin', 'GPIOOutputPin',
+           'PCF8574Input', 'PCF8574Output']
+
+from .interface import PinInterface
+from .composition import InvertingPin, SwitchPin, GuardedPin, TimerPin,\
+                         AggregatePinInterface, AbstractAggregatePin,\
+                         OrAggregatePin, AndAggregatePin
+from .gpio import GPIOInputPin, GPIOOutputPin
+from .pcf8574 import PCF8574Input, PCF8574Output
diff --git a/gpin/composition.py b/gpin/composition.py
new file mode 100644 (file)
index 0000000..30d12f3
--- /dev/null
@@ -0,0 +1,196 @@
+import abc
+import asyncio
+from typing import Sequence
+import gevent
+
+from .interface import PinInterface
+
+
+class InvertingPin(PinInterface, gevent.EventEmitterMixin):
+    ''' Wraps and inverts a pin '''
+
+    def __init__(self, pin: PinInterface) -> None:
+        self.__pin = pin
+
+        def _onChange(value: bool):
+            self._emit('change', not value)
+        self.__pin.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return not self.__pin.value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        self.__pin.value = not value
+
+    @property
+    def settable(self) -> bool:
+        return self.__pin.settable
+
+
+class SwitchPin(PinInterface, gevent.EventEmitterMixin):
+    ''' Turns a Push-Button into a Switch '''
+
+    def __init__(self, pin: PinInterface, value: bool = False) -> None:
+        self.__pin = pin
+        self.__value = value
+
+        def _onChange(value: bool):
+            if value:
+                self.value = not self.value
+        self.__pin.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return self.__value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        if value != self.__value:
+            self.__value = value
+            self._emit('change', self.__value)
+
+    @property
+    def settable(self) -> bool:
+        return True
+
+
+class GuardedPin(PinInterface, gevent.EventEmitterMixin):
+    ''' Wraps a pin and a guard '''
+
+    def __init__(self, wrapped: PinInterface, guard: PinInterface) -> None:
+        self.__wrapped = wrapped
+        self.__guard = guard
+
+        def _onChange(value: bool):
+            if self.__guard.value:
+                return
+            self._emit('change', value)
+        self.__wrapped.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return self.__wrapped.value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        if self.__guard.value:
+            return
+        self.__wrapped.value = value
+
+    @property
+    def settable(self) -> bool:
+        return self.__wrapped.settable
+
+
+class TimerPin(PinInterface, gevent.EventEmitterMixin):
+    ''' The TimerPin unsets itself after a given delay '''
+
+    def __init__(self, delay: float) -> None:
+        self.__delay = delay
+
+        self.__value = False
+        self.__handle = None
+
+    @property
+    def value(self) -> bool:
+        return self.__value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        if self.__value != value:
+            self.__value = value
+            self.__switch()
+
+            self._emit('change', self.__value)
+
+    @property
+    def settable(self) -> bool:
+        return True
+
+    def __switch(self):
+        def _trigger():
+            self.value = False
+
+        if self.__value:
+            loop = asyncio.get_running_loop()
+            self.__handle = loop.call_later(self.__delay, _trigger)
+        elif self.__handle:
+            self.__handle.cancel()
+            self.__handle = None
+
+
+class AggregatePinInterface(PinInterface):
+    ''' A pin that aggregates other pins '''
+
+    @property
+    @abc.abstractmethod
+    def children(self) -> Sequence[PinInterface]:
+        ''' The pins '''
+
+
+class AbstractAggregatePin(AggregatePinInterface,
+                           gevent.EventEmitterMixin):
+    ''' An abstract pin aggregate '''
+
+    def __init__(self, children: Sequence[PinInterface]) -> None:
+        assert children, 'AggregatePin needs at least one child!'
+
+        self.__children = tuple(children)
+        self.__value = self._calculate(self.__children)
+
+        def _onChange(_value: bool):
+            value = self.__value
+            self.__value = self._calculate(self.__children)
+            if value != self.__value:
+                self._emit('change', self.__value)
+        for pin in self.__children:
+            pin.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return self.__value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        raise NotImplementedError()
+
+    @property
+    def settable(self) -> bool:
+        return False
+
+    @property
+    def children(self) -> Sequence[PinInterface]:
+        return self.__children
+
+    @abc.abstractmethod
+    def _calculate(self, children: Sequence[PinInterface]) -> bool:
+        ''' Calculate the aggregated value '''
+        raise NotImplementedError()
+
+
+class OrAggregatePin(AbstractAggregatePin):
+    ''' A pin that aggregates with the 'or' function. '''
+
+    def __init__(self, children: Sequence[PinInterface]) -> None:
+        AbstractAggregatePin.__init__(self, children)
+
+    def _calculate(self, children: Sequence[PinInterface]) -> bool:
+        value = False
+        for child in children:
+            value = value or child.value
+        return value
+
+
+class AndAggregatePin(AbstractAggregatePin):
+    ''' A pin that aggregates with the 'and' function. '''
+
+    def __init__(self, children: Sequence[PinInterface]) -> None:
+        AbstractAggregatePin.__init__(self, children)
+
+    def _calculate(self, children: Sequence[PinInterface]) -> bool:
+        value = True
+        for child in children:
+            value = value and child.value
+        return value
diff --git a/gpin/gpio.py b/gpin/gpio.py
new file mode 100644 (file)
index 0000000..5945b01
--- /dev/null
@@ -0,0 +1,65 @@
+import asyncio
+import pigpio
+import gevent
+
+from .pigpio import get_pigpio_pi
+from .interface import PinInterface
+
+
+class GPIOInputPin(PinInterface, gevent.EventEmitterMixin):
+    def __init__(self, pin: int, glitch: int = 5000, up: bool = False) -> None:
+        self._pin = pin
+        pi = get_pigpio_pi()
+        pi.set_mode(self._pin, pigpio.INPUT)
+        pi.set_glitch_filter(self._pin, glitch)
+        pi.set_pull_up_down(self._pin,
+                            pigpio.PUD_UP if up else pigpio.PUD_DOWN)
+
+        def _onLoopChange(value: bool):
+            if self._value != value:
+                self._value = value
+                self._emit('change', self._value)
+
+        loop = asyncio.get_running_loop()
+
+        def _onGpioChange(pin: int, level: int, _tick: int):
+            if self._pin == pin and level < 2:
+                loop.call_soon_threadsafe(_onLoopChange, bool(level))
+
+        pi.callback(self._pin, pigpio.EITHER_EDGE, _onGpioChange)
+        self._value = bool(pi.read(self._pin))
+
+    @property
+    def value(self) -> bool:
+        return self._value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        raise NotImplementedError()
+
+    @property
+    def settable(self) -> bool:
+        return False
+
+
+class GPIOOutputPin(PinInterface, gevent.EventEmitterMixin):
+    def __init__(self, pin: int) -> None:
+        self._pin = pin
+        pi = get_pigpio_pi()
+        pi.set_mode(self._pin, pigpio.OUTPUT)
+        self._value = bool(pi.read(self._pin))
+
+    @property
+    def value(self) -> bool:
+        return self._value
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        if self._value != value:
+            self._value = value
+            get_pigpio_pi().write(self._pin, int(value))
+            self._emit('change', self._value)
+
+    @property
+    def settable(self) -> bool:
+        return True
diff --git a/gpin/interface.py b/gpin/interface.py
new file mode 100644 (file)
index 0000000..c1c8130
--- /dev/null
@@ -0,0 +1,23 @@
+import abc
+import gevent
+
+
+class PinInterface(gevent.EventEmitterInterface):
+    ''' Emits change(bool) '''
+
+    @property
+    @abc.abstractmethod
+    def value(self) -> bool:
+        ''' Get current pin value '''
+        raise NotImplementedError()
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        ''' Set the pin value '''
+        raise NotImplementedError()
+
+    @property
+    @abc.abstractmethod
+    def settable(self) -> bool:
+        ''' Is the pin settable? '''
+        raise NotImplementedError()
diff --git a/gpin/pcf8574.py b/gpin/pcf8574.py
new file mode 100644 (file)
index 0000000..9e737e9
--- /dev/null
@@ -0,0 +1,136 @@
+import gevent
+from typing import Callable
+
+from .pigpio import get_pigpio_pi
+from .interface import PinInterface
+
+PCF_ADDRESSES = tuple(range(32, 40)) + tuple(range(56, 64))
+
+
+def emitDiff(emit: Callable, oldValues: int, newValues: int):
+    assert isinstance(oldValues, int), 'oldValues must be an integer'
+    assert oldValues >= 0 and oldValues <= 255,\
+        'oldValues must be >= 0 and <= 255'
+    assert isinstance(newValues, int), 'newValues must be an integer'
+    assert newValues >= 0 and newValues <= 255,\
+        'newValues must be >= 0 and <= 255'
+    for i in range(0, 8):
+        mask = 1 << i
+        if mask & oldValues != mask & newValues:
+            emit('change', i, not bool(mask & newValues))
+
+
+class PCF8574Input(gevent.EventEmitterMixin):
+    def __init__(self, address: int, interrupt: PinInterface) -> None:
+        assert address in PCF_ADDRESSES, 'Invalid PCF8574(A) I²C address'
+        self._address = address
+        self._interrupt = interrupt
+        pi = get_pigpio_pi()
+        self._handle = pi.i2c_open(1, self._address)
+        self._values = pi.i2c_read_byte(self._handle)
+        self._pins = tuple(PCF8574InputPin(self, i) for i in range(0, 8))
+
+        def _onInterrupt(_value: bool):
+            oldValues = self._values
+            self._values = pi.i2c_read_byte(self._handle)
+            emitDiff(self._emit, oldValues, self._values)
+        self._int_handle = self._interrupt.on('change', _onInterrupt)
+
+    def close(self) -> None:
+        self._interrupt.off(self._int_handle)
+        try:
+            get_pigpio_pi().i2c_close(self._handle)
+        except AttributeError:
+            pass
+
+    def getPin(self, pin: int) -> PinInterface:
+        assert isinstance(pin, int), 'pin must be an integer'
+        assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7'
+        return self._pins[pin]
+
+    def getValue(self, pin: int) -> bool:
+        assert isinstance(pin, int), 'pin must be an integer'
+        assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7'
+        return not bool(self._values & (1 << pin))
+
+
+class PCF8574InputPin(PinInterface, gevent.EventEmitterMixin):
+    def __init__(self, pcfInput: PCF8574Input, pcfPin: int) -> None:
+        self._input = pcfInput
+        self._pin = pcfPin
+
+        def _onChange(pin: int, value: int):
+            if self._pin == pin:
+                self._emit('change', value)
+        self._input.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return self._input.getValue(self._pin)
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        raise NotImplementedError()
+
+    @property
+    def settable(self) -> bool:
+        return False
+
+
+class PCF8574Output(gevent.EventEmitterMixin):
+    def __init__(self, address: int) -> None:
+        assert address in PCF_ADDRESSES, 'Invalid PCF8574(A) I²C address'
+        self._address = address
+        pi = get_pigpio_pi()
+        self._handle = pi.i2c_open(1, self._address)
+        self._values = pi.i2c_read_byte(self._handle)
+        self._pins = tuple(PCF8574OutputPin(self, i) for i in range(0, 8))
+
+    def close(self) -> None:
+        try:
+            get_pigpio_pi().i2c_close(self._handle)
+        except AttributeError:
+            pass
+
+    def getPin(self, pin: int) -> PinInterface:
+        assert isinstance(pin, int), 'pin must be an integer'
+        assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7'
+        return self._pins[pin]
+
+    def getValue(self, pin: int) -> bool:
+        assert isinstance(pin, int), 'pin must be an integer'
+        assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7'
+        return not bool(self._values & (1 << pin))
+
+    def setValue(self, pin: int, value: bool) -> None:
+        assert isinstance(pin, int), 'pin must be an integer'
+        assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7'
+        assert isinstance(value, bool), 'value must be a bool'
+        value = not value
+        oldValues = self._values
+        self._values = (oldValues & (0xFF ^ (1 << pin))) | (int(value) << pin)
+        get_pigpio_pi().i2c_write_byte(self._handle, self._values)
+        emitDiff(self._emit, oldValues, self._values)
+
+
+class PCF8574OutputPin(PinInterface, gevent.EventEmitterMixin):
+    def __init__(self, pcfOutput: PCF8574Output, pcfPin: int) -> None:
+        self._output = pcfOutput
+        self._pin = pcfPin
+
+        def _onChange(pin: int, value: bool):
+            if self._pin == pin:
+                self._emit('change', value)
+        self._output.on('change', _onChange)
+
+    @property
+    def value(self) -> bool:
+        return self._output.getValue(self._pin)
+
+    @value.setter
+    def value(self, value: bool) -> None:
+        self._output.setValue(self._pin, value)
+
+    @property
+    def settable(self) -> bool:
+        return True
diff --git a/gpin/pigpio.py b/gpin/pigpio.py
new file mode 100644 (file)
index 0000000..0e851c1
--- /dev/null
@@ -0,0 +1,9 @@
+import pigpio
+
+_pigpio_pi = None
+
+def get_pigpio_pi():
+    global _pigpio_pi
+    if _pigpio_pi is None:
+        _pigpio_pi = pigpio.pi()
+    return _pigpio_pi
diff --git a/graphit_controlpi/__init__.py b/graphit_controlpi/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/graphit_controlpi/config.py b/graphit_controlpi/config.py
deleted file mode 100644 (file)
index 3b44e77..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-from graphit_pin import PCF8574Output, PCF8574Input, GPIOInputPin
-from graphit_modbus.transport import SerialPort, SerialClient
-from graphit_modbus.hitachi import SJP1Fu
-
-
-async def process_configuration(conf, queues):
-
-    def callback_factory(pin_name):
-        settable = pins[pin_name].settable
-
-        def callback(value):
-            for queue in queues:
-                queue.put_nowait({'event': 'pinstate', 'pin': pin_name,
-                                  'settable': settable, 'value': value,
-                                  'changed': True})
-        return callback
-    pins = {}
-    if 'i/o cards' in conf:
-        for card_conf in conf['i/o cards']:
-            card = None
-            if card_conf['type'] == 'output':
-                card = PCF8574Output(card_conf['address'])
-            elif card_conf['type'] == 'input':
-                card = PCF8574Input(card_conf['address'],
-                                    GPIOInputPin(card_conf['interrupt pin'],
-                                                 up=True))
-            if card is not None:
-                for i in range(8):
-                    pin = card.getPin(i)
-                    pin_names = card_conf['pins'][i]
-                    for pin_name in pin_names:
-                        pins[pin_name] = pin
-                        pin.on('change', callback_factory(pin_name))
-    fu = None
-    if 'modbus' in conf:
-        modbus_conf = conf['modbus']
-        port = SerialPort(modbus_conf['serial device'])
-        client = SerialClient(port, modbus_conf['slave id'])
-        fu = SJP1Fu(client)
-    return (pins, fu)
diff --git a/graphit_controlpi/main.py b/graphit_controlpi/main.py
deleted file mode 100644 (file)
index d50df05..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import sys
-import json
-import asyncio
-
-from .config import process_configuration
-from .websocket import setup_websocket
-
-
-async def setup():
-    pins = {}
-    fu = None
-    queues = []
-    with open(sys.argv[1]) as json_data:
-        conf = json.load(json_data)
-        (pins, fu) = await process_configuration(conf, queues)
-    await setup_websocket(pins, fu, queues, sys.argv[2])
-
-
-if __name__ == '__main__':
-    asyncio.get_event_loop().run_until_complete(setup())
-    try:
-        asyncio.get_event_loop().run_forever()
-    except KeyboardInterrupt:
-        pass
diff --git a/graphit_controlpi/websocket.py b/graphit_controlpi/websocket.py
deleted file mode 100644 (file)
index 6d3d000..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-import os
-import functools
-import socket
-import json
-import asyncio
-import websockets
-from http import HTTPStatus
-
-
-async def process_command(command, pins, fu, queue):
-    if command['command'] == 'setpin':
-        if command['pin'] in pins and pins[command['pin']].settable:
-            pins[command['pin']].value = command['value']
-    elif command['command'] == 'getpin':
-        if command['pin'] in pins:
-            pin = pins[command['pin']]
-            await queue.put({'event': 'pinstate', 'pin': command['pin'],
-                             'settable': pin.settable, 'value': pin.value,
-                             'changed': False})
-    elif command['command'] == 'getallpins':
-        for pin_name in pins:
-            pin = pins[pin_name]
-            await queue.put({'event': 'pinstate', 'pin': pin_name,
-                             'settable': pin.settable, 'value': pin.value,
-                             'changed': False})
-    elif command['command'] == 'setfrequency':
-        await fu.set_frequency(command['value'])
-    elif command['command'] == 'getfrequency':
-        frequency = await fu.get_frequency()
-        await queue.put({'event': 'frequency', 'frequency': frequency})
-    elif command['command'] == 'startinverter':
-        await fu.start_inverter()
-    elif command['command'] == 'stopinverter':
-        await fu.stop_inverter()
-    elif command['command'] == 'getinverter':
-        active = await fu.inverter_active
-        await queue.put({'event': 'inverterstate', 'active': active})
-
-
-async def command_handler(websocket, path, pins, fu, queue):
-    async for message in websocket:
-        command = json.loads(message)
-        await process_command(command, pins, fu, queue)
-
-
-async def event_handler(websocket, path, queue):
-    while True:
-        event = await queue.get()
-        message = json.dumps(event)
-        await websocket.send(message)
-        queue.task_done()
-
-
-async def handler(pins, fu, queues, websocket, path):
-    queue = asyncio.Queue()
-    queues.append(queue)
-    command_task = asyncio.create_task(command_handler(websocket, path,
-                                                       pins, fu, queue))
-    event_task = asyncio.create_task(event_handler(websocket, path,
-                                                   queue))
-    done, pending = await asyncio.wait(
-        [command_task, event_task],
-        return_when=asyncio.FIRST_COMPLETED,
-    )
-    for task in pending:
-        task.cancel()
-    queues.remove(queue)
-
-
-async def process_request(server_root, path, request_headers):
-    if 'Upgrade' in request_headers:
-        return
-    if path == '/':
-        path = '/index.html'
-    response_headers = [
-        ('Server', 'graphit_controlpi websocket server'),
-        ('Connection', 'close'),
-    ]
-    server_root = os.path.realpath(os.path.join(os.getcwd(), server_root))
-    full_path = os.path.realpath(os.path.join(server_root, path[1:]))
-    if os.path.commonpath((server_root, full_path)) != server_root or \
-            not os.path.exists(full_path) or not os.path.isfile(full_path):
-        return HTTPStatus.NOT_FOUND, [], b'404 NOT FOUND'
-    mime_type = 'application/octet-stream'
-    extension = full_path.split(".")[-1]
-    if extension == 'html':
-        mime_type = 'text/html'
-    elif extension == 'js':
-        mime_type = 'text/javascript'
-    elif extension == 'css':
-        mime_type = 'text/css'
-    response_headers.append(('Content-Type', mime_type))
-    body = open(full_path, 'rb').read()
-    response_headers.append(('Content-Length', str(len(body))))
-    return HTTPStatus.OK, response_headers, body
-
-
-async def get_ip():
-    ip = None
-    while not ip:
-        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        try:
-            s.connect(('10.255.255.255', 1))
-            ip = s.getsockname()[0]
-        except Exception:
-            ip = None
-            await asyncio.sleep(0.1)
-        finally:
-            s.close()
-    return ip
-
-
-async def setup_websocket(pins, fu, queues, server_root):
-    parameterised_handler = functools.partial(handler, pins, fu, queues)
-    parameterised_process_request = functools.partial(process_request, server_root)
-    hostname = await get_ip()
-    await websockets.serve(parameterised_handler, hostname, 80,
-                           process_request=parameterised_process_request)
-    print(f"Serving on ws://{hostname}:80")
diff --git a/modbus_test.py b/modbus_test.py
new file mode 100644 (file)
index 0000000..1cf1ba2
--- /dev/null
@@ -0,0 +1,35 @@
+import sys
+import json
+import asyncio
+import websockets
+
+
+async def test_commands(websocket):
+    commands = [{'command': 'getfrequency'},
+                {'command': 'setfrequency', 'value': 100},
+                {'command': 'getfrequency'},
+                {'command': 'getinverter'},
+                {'command': 'startinverter'},
+                {'command': 'getinverter'},
+                {'command': 'stopinverter'},
+                {'command': 'getinverter'}]
+    for command in commands:
+        message = json.dumps(command)
+        await websocket.send(message)
+        print(f"Sent Command: {message}")
+        await asyncio.sleep(1)
+
+
+async def receive_events(websocket):
+    async for message in websocket:
+        print(f"Recvd. Event: {message}")
+
+
+async def main(hostname):
+    async with websockets.connect(f"ws://{hostname}") as websocket:
+        command_task = asyncio.create_task(test_commands(websocket))
+        event_task = asyncio.create_task(receive_events(websocket))
+        await command_task
+
+if __name__ == '__main__':
+    asyncio.run(main(sys.argv[1]))
diff --git a/schaltschrank.service b/schaltschrank.service
new file mode 100644 (file)
index 0000000..04fb234
--- /dev/null
@@ -0,0 +1,12 @@
+[Unit]
+Description=Schaltschrank Service
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+WorkingDirectory=/home/pi
+Environment=PYTHONUNBUFFERED=1
+ExecStart=/home/pi/schaltschrank/bin/python -m schaltschrank.main conf.json web/
+
+[Install]
+WantedBy=multi-user.target
diff --git a/schaltschrank/__init__.py b/schaltschrank/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/schaltschrank/config.py b/schaltschrank/config.py
new file mode 100644 (file)
index 0000000..d358d06
--- /dev/null
@@ -0,0 +1,40 @@
+from gpin import PCF8574Output, PCF8574Input, GPIOInputPin
+from gmodbus.transport import SerialPort, SerialClient
+from gmodbus.hitachi import SJP1Fu
+
+
+async def process_configuration(conf, queues):
+
+    def callback_factory(pin_name):
+        settable = pins[pin_name].settable
+
+        def callback(value):
+            for queue in queues:
+                queue.put_nowait({'event': 'pinstate', 'pin': pin_name,
+                                  'settable': settable, 'value': value,
+                                  'changed': True})
+        return callback
+    pins = {}
+    if 'i/o cards' in conf:
+        for card_conf in conf['i/o cards']:
+            card = None
+            if card_conf['type'] == 'output':
+                card = PCF8574Output(card_conf['address'])
+            elif card_conf['type'] == 'input':
+                card = PCF8574Input(card_conf['address'],
+                                    GPIOInputPin(card_conf['interrupt pin'],
+                                                 up=True))
+            if card is not None:
+                for i in range(8):
+                    pin = card.getPin(i)
+                    pin_names = card_conf['pins'][i]
+                    for pin_name in pin_names:
+                        pins[pin_name] = pin
+                        pin.on('change', callback_factory(pin_name))
+    fu = None
+    if 'modbus' in conf:
+        modbus_conf = conf['modbus']
+        port = SerialPort(modbus_conf['serial device'])
+        client = SerialClient(port, modbus_conf['slave id'])
+        fu = SJP1Fu(client)
+    return (pins, fu)
diff --git a/schaltschrank/main.py b/schaltschrank/main.py
new file mode 100644 (file)
index 0000000..d50df05
--- /dev/null
@@ -0,0 +1,24 @@
+import sys
+import json
+import asyncio
+
+from .config import process_configuration
+from .websocket import setup_websocket
+
+
+async def setup():
+    pins = {}
+    fu = None
+    queues = []
+    with open(sys.argv[1]) as json_data:
+        conf = json.load(json_data)
+        (pins, fu) = await process_configuration(conf, queues)
+    await setup_websocket(pins, fu, queues, sys.argv[2])
+
+
+if __name__ == '__main__':
+    asyncio.get_event_loop().run_until_complete(setup())
+    try:
+        asyncio.get_event_loop().run_forever()
+    except KeyboardInterrupt:
+        pass
diff --git a/schaltschrank/websocket.py b/schaltschrank/websocket.py
new file mode 100644 (file)
index 0000000..6d3d000
--- /dev/null
@@ -0,0 +1,119 @@
+import os
+import functools
+import socket
+import json
+import asyncio
+import websockets
+from http import HTTPStatus
+
+
+async def process_command(command, pins, fu, queue):
+    if command['command'] == 'setpin':
+        if command['pin'] in pins and pins[command['pin']].settable:
+            pins[command['pin']].value = command['value']
+    elif command['command'] == 'getpin':
+        if command['pin'] in pins:
+            pin = pins[command['pin']]
+            await queue.put({'event': 'pinstate', 'pin': command['pin'],
+                             'settable': pin.settable, 'value': pin.value,
+                             'changed': False})
+    elif command['command'] == 'getallpins':
+        for pin_name in pins:
+            pin = pins[pin_name]
+            await queue.put({'event': 'pinstate', 'pin': pin_name,
+                             'settable': pin.settable, 'value': pin.value,
+                             'changed': False})
+    elif command['command'] == 'setfrequency':
+        await fu.set_frequency(command['value'])
+    elif command['command'] == 'getfrequency':
+        frequency = await fu.get_frequency()
+        await queue.put({'event': 'frequency', 'frequency': frequency})
+    elif command['command'] == 'startinverter':
+        await fu.start_inverter()
+    elif command['command'] == 'stopinverter':
+        await fu.stop_inverter()
+    elif command['command'] == 'getinverter':
+        active = await fu.inverter_active
+        await queue.put({'event': 'inverterstate', 'active': active})
+
+
+async def command_handler(websocket, path, pins, fu, queue):
+    async for message in websocket:
+        command = json.loads(message)
+        await process_command(command, pins, fu, queue)
+
+
+async def event_handler(websocket, path, queue):
+    while True:
+        event = await queue.get()
+        message = json.dumps(event)
+        await websocket.send(message)
+        queue.task_done()
+
+
+async def handler(pins, fu, queues, websocket, path):
+    queue = asyncio.Queue()
+    queues.append(queue)
+    command_task = asyncio.create_task(command_handler(websocket, path,
+                                                       pins, fu, queue))
+    event_task = asyncio.create_task(event_handler(websocket, path,
+                                                   queue))
+    done, pending = await asyncio.wait(
+        [command_task, event_task],
+        return_when=asyncio.FIRST_COMPLETED,
+    )
+    for task in pending:
+        task.cancel()
+    queues.remove(queue)
+
+
+async def process_request(server_root, path, request_headers):
+    if 'Upgrade' in request_headers:
+        return
+    if path == '/':
+        path = '/index.html'
+    response_headers = [
+        ('Server', 'graphit_controlpi websocket server'),
+        ('Connection', 'close'),
+    ]
+    server_root = os.path.realpath(os.path.join(os.getcwd(), server_root))
+    full_path = os.path.realpath(os.path.join(server_root, path[1:]))
+    if os.path.commonpath((server_root, full_path)) != server_root or \
+            not os.path.exists(full_path) or not os.path.isfile(full_path):
+        return HTTPStatus.NOT_FOUND, [], b'404 NOT FOUND'
+    mime_type = 'application/octet-stream'
+    extension = full_path.split(".")[-1]
+    if extension == 'html':
+        mime_type = 'text/html'
+    elif extension == 'js':
+        mime_type = 'text/javascript'
+    elif extension == 'css':
+        mime_type = 'text/css'
+    response_headers.append(('Content-Type', mime_type))
+    body = open(full_path, 'rb').read()
+    response_headers.append(('Content-Length', str(len(body))))
+    return HTTPStatus.OK, response_headers, body
+
+
+async def get_ip():
+    ip = None
+    while not ip:
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        try:
+            s.connect(('10.255.255.255', 1))
+            ip = s.getsockname()[0]
+        except Exception:
+            ip = None
+            await asyncio.sleep(0.1)
+        finally:
+            s.close()
+    return ip
+
+
+async def setup_websocket(pins, fu, queues, server_root):
+    parameterised_handler = functools.partial(handler, pins, fu, queues)
+    parameterised_process_request = functools.partial(process_request, server_root)
+    hostname = await get_ip()
+    await websockets.serve(parameterised_handler, hostname, 80,
+                           process_request=parameterised_process_request)
+    print(f"Serving on ws://{hostname}:80")
index d0cbf303ebcbf05a125af07d18a75afd324598b4..3ba7f44f37670a8b2bb0813de4ee0862ff604ff4 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as readme_file:
 
 setuptools.setup(
     name="graphit-controlpi",
-    version="0.2.3",
+    version="0.2.4",
     author="Graph-IT GmbH",
     author_email="info@graph-it.com",
     description="Main Module for Machine Control Pi",
@@ -18,7 +18,8 @@ setuptools.setup(
     ],
     install_requires=[
         "websockets",
-        "graphit-pin @ git+git://git.graph-it.com/graphit/pin-py.git",
+        "pigpio",
+        "umodbus",
     ],
     classifiers=[
         "Programming Language :: Python",