Compare commits
5 commits
7861fff539
...
4b971c01a6
Author | SHA1 | Date | |
---|---|---|---|
4b971c01a6 | |||
3644a0c441 | |||
87336262d8 | |||
83439ecfa4 | |||
01aa870c55 |
14 changed files with 246 additions and 72 deletions
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.1.3
|
current_version = 0.1.4
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,3 @@ grpcio-tools~=1.43.0
|
||||||
mypy-protobuf~=3.2.0
|
mypy-protobuf~=3.2.0
|
||||||
types-protobuf~=3.19.6
|
types-protobuf~=3.19.6
|
||||||
grpc-stubs~=1.24.7
|
grpc-stubs~=1.24.7
|
||||||
bump2version
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ with open('README.rst') as f:
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='TSGRain Controller',
|
name='TSGRain Controller',
|
||||||
version='0.1.3',
|
version='0.1.4',
|
||||||
author='ThetaDev',
|
author='ThetaDev',
|
||||||
description='TSGRain irrigation controller',
|
description='TSGRain irrigation controller',
|
||||||
long_description=README,
|
long_description=README,
|
||||||
|
|
|
@ -120,6 +120,20 @@ def test_start_task_queue(app):
|
||||||
assert tasks[0].zone_id == 2
|
assert tasks[0].zone_id == 2
|
||||||
assert tasks[1].zone_id == 3
|
assert tasks[1].zone_id == 3
|
||||||
|
|
||||||
|
# Queue processing time
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Try to enqueue the same task again -> should cancel
|
||||||
|
res = app.request_task(
|
||||||
|
models.TaskRequest(source=models.Source.MANUAL,
|
||||||
|
zone_id=3,
|
||||||
|
duration=30,
|
||||||
|
queuing=True,
|
||||||
|
cancelling=True))
|
||||||
|
|
||||||
|
assert not res.started
|
||||||
|
assert res.stopped
|
||||||
|
|
||||||
|
|
||||||
def test_crud_job(app):
|
def test_crud_job(app):
|
||||||
# Insert jobs
|
# Insert jobs
|
||||||
|
|
22
tests/test_jobschedule.py
Normal file
22
tests/test_jobschedule.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# coding=utf-8
|
||||||
|
from datetime import datetime
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tsgrain_controller import models
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('date, repeat, expect', [
|
||||||
|
(datetime(2022, 1, 18, 10, 30, 0), False, True),
|
||||||
|
(datetime(2022, 1, 18, 10, 30, 13), False, True),
|
||||||
|
(datetime(2022, 1, 18, 10, 29, 59), False, False),
|
||||||
|
(datetime(2022, 1, 19, 10, 30, 0), False, False),
|
||||||
|
(datetime(2022, 1, 19, 10, 30, 0), True, True),
|
||||||
|
])
|
||||||
|
def test_job_check(date, repeat, expect):
|
||||||
|
date_now = datetime(2022, 1, 18, 10, 30, 0)
|
||||||
|
|
||||||
|
job = models.Job(date, 30, [1], True, repeat)
|
||||||
|
assert job.check(date_now) is expect
|
||||||
|
|
||||||
|
job.enable = False
|
||||||
|
assert job.check(date_now) is False
|
|
@ -80,3 +80,30 @@ def test_queue_runner():
|
||||||
assert util.to_json(q) == \
|
assert util.to_json(q) == \
|
||||||
'[{"source": "MANUAL", "zone_id": 2, "duration": 5, "remaining": 4}, \
|
'[{"source": "MANUAL", "zone_id": 2, "duration": 5, "remaining": 4}, \
|
||||||
{"source": "SCHEDULE", "zone_id": 2, "duration": 10, "remaining": 10}]'
|
{"source": "SCHEDULE", "zone_id": 2, "duration": 10, "remaining": 10}]'
|
||||||
|
|
||||||
|
|
||||||
|
def test_interrupt_auto():
|
||||||
|
"""
|
||||||
|
Wenn der Automatikmodus unterbrochen und ein Scheduled-Task unterbrochen in der
|
||||||
|
Warteschlange liegt, sollte ein manueller Task gestartet werden können.
|
||||||
|
"""
|
||||||
|
app = fixtures.TestingApp()
|
||||||
|
q = task_queue.TaskQueue(app)
|
||||||
|
app.auto = True
|
||||||
|
|
||||||
|
taskAuto = models.Task(models.Source.SCHEDULE, 1, 10)
|
||||||
|
taskMan = models.Task(models.Source.MANUAL, 2, 5)
|
||||||
|
|
||||||
|
q.start()
|
||||||
|
|
||||||
|
assert q.enqueue(taskAuto)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
assert not q.enqueue(taskMan, False)
|
||||||
|
|
||||||
|
app.auto = False
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
assert q.enqueue(taskMan, False)
|
||||||
|
|
||||||
|
q.stop()
|
||||||
|
|
|
@ -20,7 +20,7 @@ def test_run_cmd(cmd_str: str, raise_err: bool):
|
||||||
systimecfg._run_cmd(cmd_str)
|
systimecfg._run_cmd(cmd_str)
|
||||||
|
|
||||||
|
|
||||||
def get_system_timezone(mocker):
|
def test_get_system_timezone(mocker):
|
||||||
mock_res = mock.Mock()
|
mock_res = mock.Mock()
|
||||||
mock_res.stdout = 'Europe/Berlin'
|
mock_res.stdout = 'Europe/Berlin'
|
||||||
|
|
||||||
|
@ -38,6 +38,16 @@ def get_system_timezone(mocker):
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_system_timezone_err(mocker):
|
||||||
|
mock_res = mock.Mock()
|
||||||
|
mock_res.stdout = 'Europe Berlin'
|
||||||
|
|
||||||
|
mocker.patch('subprocess.run', return_value=mock_res)
|
||||||
|
|
||||||
|
with pytest.raises(systimecfg.ErrorInvalidTimezone):
|
||||||
|
systimecfg.get_system_timezone()
|
||||||
|
|
||||||
|
|
||||||
def test_set_system_datetime(mocker):
|
def test_set_system_datetime(mocker):
|
||||||
cmd_run_mock: mock.MagicMock = mocker.patch('subprocess.run')
|
cmd_run_mock: mock.MagicMock = mocker.patch('subprocess.run')
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.1.3'
|
__version__ = '0.1.4'
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Application(models.AppInterface):
|
||||||
self.db.load_queue(self.queue)
|
self.db.load_queue(self.queue)
|
||||||
|
|
||||||
self.io = io_factory.new_io(self, io_type)
|
self.io = io_factory.new_io(self, io_type)
|
||||||
self.io.set_callbacks(self._cb_manual, self._cb_modekey)
|
self.io.set_callback(self._input_cb)
|
||||||
|
|
||||||
self.outputs = output.Outputs(self.io, self.queue, self)
|
self.outputs = output.Outputs(self.io, self.queue, self)
|
||||||
|
|
||||||
|
@ -71,16 +71,21 @@ class Application(models.AppInterface):
|
||||||
def get_logger(self) -> logging.Logger:
|
def get_logger(self) -> logging.Logger:
|
||||||
return self.logger
|
return self.logger
|
||||||
|
|
||||||
def _cb_manual(self, zone_id: int):
|
def _input_cb(self, key: str):
|
||||||
self.request_task(
|
if key == 'BT_MODE':
|
||||||
models.TaskRequest(source=models.Source.MANUAL,
|
self.set_auto_mode(not self.get_auto_mode())
|
||||||
zone_id=zone_id,
|
elif key.startswith('BT_Z_'):
|
||||||
duration=self.cfg.manual_time,
|
zoneid_str = key[5:]
|
||||||
queuing=False,
|
try:
|
||||||
cancelling=True))
|
zone_id = int(zoneid_str)
|
||||||
|
except ValueError:
|
||||||
def _cb_modekey(self):
|
return
|
||||||
self.set_auto_mode(not self.get_auto_mode())
|
self.request_task(
|
||||||
|
models.TaskRequest(source=models.Source.MANUAL,
|
||||||
|
zone_id=zone_id,
|
||||||
|
duration=self.cfg.manual_time,
|
||||||
|
queuing=False,
|
||||||
|
cancelling=True))
|
||||||
|
|
||||||
def request_task(self,
|
def request_task(self,
|
||||||
request: models.TaskRequest) -> models.TaskRequestResult:
|
request: models.TaskRequest) -> models.TaskRequestResult:
|
||||||
|
@ -106,7 +111,7 @@ class Application(models.AppInterface):
|
||||||
|
|
||||||
task = models.Task(request.source, request.zone_id, duration)
|
task = models.Task(request.source, request.zone_id, duration)
|
||||||
task.validate(self)
|
task.validate(self)
|
||||||
started = self.queue.enqueue(task, not request.queuing)
|
started = self.queue.enqueue(task, request.queuing)
|
||||||
return models.TaskRequestResult(started, False)
|
return models.TaskRequestResult(started, False)
|
||||||
|
|
||||||
def start_task(self, source: models.Source, zone_id: int, duration: int,
|
def start_task(self, source: models.Source, zone_id: int, duration: int,
|
||||||
|
@ -171,6 +176,7 @@ class Application(models.AppInterface):
|
||||||
systimecfg.set_system_timezone(tz, self.cfg.cmd_set_timezone)
|
systimecfg.set_system_timezone(tz, self.cfg.cmd_set_timezone)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
logging.info('Starting application')
|
||||||
self._running = True
|
self._running = True
|
||||||
self.io.start()
|
self.io.start()
|
||||||
self.outputs.start()
|
self.outputs.start()
|
||||||
|
@ -179,6 +185,7 @@ class Application(models.AppInterface):
|
||||||
self.grpc_server.start()
|
self.grpc_server.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
logging.info('Stopping application')
|
||||||
self._running = False
|
self._running = False
|
||||||
self.grpc_server.stop(None)
|
self.grpc_server.stop(None)
|
||||||
self.scheduler.stop()
|
self.scheduler.stop()
|
||||||
|
|
|
@ -5,27 +5,36 @@ from typing import Callable, Optional
|
||||||
class Io:
|
class Io:
|
||||||
|
|
||||||
def __init__(self, *args): # pylint: disable=unused-argument
|
def __init__(self, *args): # pylint: disable=unused-argument
|
||||||
self.cb_manual: Optional[Callable[[int], None]] = None
|
self.cb_input: Optional[Callable[[str], None]] = None
|
||||||
self.cb_mode: Optional[Callable[[], None]] = None
|
|
||||||
|
|
||||||
def set_callbacks(self, cb_manual: Optional[Callable[[int], None]],
|
def set_callback(self, cb: Optional[Callable[[str], None]]):
|
||||||
cb_mode: Optional[Callable[[], None]]):
|
"""
|
||||||
self.cb_manual = cb_manual
|
Setze die Callback-Funktion, die bei einer Eingabe aufgerufen wird.
|
||||||
self.cb_mode = cb_mode
|
Als Parameter wird der Name des Eingabegeräts mit übergeben.
|
||||||
|
|
||||||
def _trigger_cb_manual(self, zone_id: int):
|
:param cb: Input-Callback-Funktion
|
||||||
if self.cb_manual is not None:
|
"""
|
||||||
self.cb_manual(zone_id)
|
self.cb_input = cb
|
||||||
|
|
||||||
def _trigger_cb_mode(self):
|
def _trigger_cb(self, key: str):
|
||||||
if self.cb_mode is not None:
|
"""
|
||||||
self.cb_mode()
|
Löse die Input-Callback-Funktion aus
|
||||||
|
|
||||||
|
:param key: Gerätename
|
||||||
|
"""
|
||||||
|
if self.cb_input is not None:
|
||||||
|
self.cb_input(key)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
pass
|
"""Initialisiere die IO"""
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
"""Beende die IO und deaktiviere alle Ausgabegeräte"""
|
||||||
|
|
||||||
def write_output(self, key: str, val: bool):
|
def write_output(self, key: str, val: bool):
|
||||||
pass
|
"""
|
||||||
|
Setze den Zustand eines Ausgabegeräts
|
||||||
|
|
||||||
|
:param key: Name des Ausgabegeräts (z.B. ``VALVE_1``)
|
||||||
|
:param val: Status des Ausgabegeräts
|
||||||
|
"""
|
||||||
|
|
|
@ -77,10 +77,10 @@ class Io(util.StoppableThread, io.Io):
|
||||||
|
|
||||||
# Mode key (0)
|
# Mode key (0)
|
||||||
if c == 48:
|
if c == 48:
|
||||||
self._trigger_cb_mode()
|
self._trigger_cb('BT_MODE')
|
||||||
# Zone keys (1-7)
|
# Zone keys (1-7)
|
||||||
elif 49 <= c <= 55:
|
elif 49 <= c <= 55:
|
||||||
self._trigger_cb_manual(c - 48)
|
self._trigger_cb(f'BT_Z_{c - 48}')
|
||||||
|
|
||||||
self._screen.erase()
|
self._screen.erase()
|
||||||
self._screen.addstr(0, 0,
|
self._screen.addstr(0, 0,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable, Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from RPi import GPIO # pylint: disable=import-error
|
from RPi import GPIO # pylint: disable=import-error
|
||||||
import smbus
|
import smbus
|
||||||
|
@ -38,7 +38,8 @@ Polarität pro GPIOA-Pin.
|
||||||
- ``0`` High wenn aktiv
|
- ``0`` High wenn aktiv
|
||||||
|
|
||||||
Die Einstellung hatte im Test nur bei als Eingängen konfigurierten Pins
|
Die Einstellung hatte im Test nur bei als Eingängen konfigurierten Pins
|
||||||
einen Effekt. Bei Ausgängen wird stattdessen der gesetzte Wert invertiert.
|
einen Effekt. Invertierte Ausgänge setzt die Library deswegen auf den
|
||||||
|
entgegengesetzten Wert.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MCP_IPOLB = 0x03
|
MCP_IPOLB = 0x03
|
||||||
|
@ -197,6 +198,8 @@ class PinConfigInvalid(Exception):
|
||||||
|
|
||||||
|
|
||||||
class _MCP23017Port(Enum):
|
class _MCP23017Port(Enum):
|
||||||
|
"""IO-Port des MCP23017 (A/B)"""
|
||||||
|
|
||||||
A = 0
|
A = 0
|
||||||
B = 1
|
B = 1
|
||||||
|
|
||||||
|
@ -206,13 +209,41 @@ class _MCP23017Port(Enum):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _MCP23017Device:
|
class _MCP23017Device:
|
||||||
|
"""
|
||||||
|
Ein einzelnes mit einen MCP23017 I2C-Portexpander verbundenes
|
||||||
|
Eingabe/Ausgabegerät.
|
||||||
|
"""
|
||||||
|
|
||||||
i2c_address: int
|
i2c_address: int
|
||||||
|
"""I2C-Adresse des MCP23017"""
|
||||||
|
|
||||||
port: _MCP23017Port
|
port: _MCP23017Port
|
||||||
|
"""IO-Port des MCP23017 (A/B)"""
|
||||||
|
|
||||||
pin: int
|
pin: int
|
||||||
|
"""IO-Pin des MCP23017 (0-7)"""
|
||||||
|
|
||||||
invert: bool
|
invert: bool
|
||||||
|
"""Zustand des Pins invertieren"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, cfg_str: str):
|
def from_config(cls, cfg_str: str) -> '_MCP23017Device':
|
||||||
|
"""
|
||||||
|
Parst einen Konfigurationsstring und erstellt daraus
|
||||||
|
ein neues ``_MCP23017Device``-Objekt.
|
||||||
|
|
||||||
|
Der Konfigurationsstring hat folgendes Format:
|
||||||
|
``'I2C_ADDR/PIN'``
|
||||||
|
|
||||||
|
Beispiel: ``0x27/B0`` (MCP23017 mit I2C-Adresse 0x27, Pin B0)
|
||||||
|
|
||||||
|
Um den Zustand eines Geräts zu invertieren, einfach ``/!``
|
||||||
|
an den Konfiguraionsstring anfügen: ``0x27/B0/!``
|
||||||
|
|
||||||
|
:param cfg_str: Konfigurationsstring
|
||||||
|
:return: Neues ``_MCP23017Device``-Objekt
|
||||||
|
"""
|
||||||
|
|
||||||
cfg_parts = cfg_str.split('/')
|
cfg_parts = cfg_str.split('/')
|
||||||
if len(cfg_parts) < 2:
|
if len(cfg_parts) < 2:
|
||||||
raise PinConfigInvalid(cfg_str)
|
raise PinConfigInvalid(cfg_str)
|
||||||
|
@ -251,8 +282,6 @@ class Io(io.Io):
|
||||||
def __init__(self, app: models.AppInterface):
|
def __init__(self, app: models.AppInterface):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.cb_manual: Optional[Callable[[int], None]] = None
|
|
||||||
self.cb_mode: Optional[Callable[[], None]] = None
|
|
||||||
self.cfg = app.get_cfg()
|
self.cfg = app.get_cfg()
|
||||||
|
|
||||||
self.bus = smbus.SMBus(self.cfg.i2c_bus_id)
|
self.bus = smbus.SMBus(self.cfg.i2c_bus_id)
|
||||||
|
@ -263,7 +292,7 @@ class Io(io.Io):
|
||||||
self.output_devices: Dict[str, _MCP23017Device] = {}
|
self.output_devices: Dict[str, _MCP23017Device] = {}
|
||||||
self.input_devices: Dict[str, _MCP23017Device] = {}
|
self.input_devices: Dict[str, _MCP23017Device] = {}
|
||||||
|
|
||||||
# Parse config
|
# Parse config and initialize pins
|
||||||
for key, cfg_str in self.cfg.input_devices.items():
|
for key, cfg_str in self.cfg.input_devices.items():
|
||||||
device = _MCP23017Device.from_config(cfg_str)
|
device = _MCP23017Device.from_config(cfg_str)
|
||||||
self.mcp_addresses.add(device.i2c_address)
|
self.mcp_addresses.add(device.i2c_address)
|
||||||
|
@ -278,6 +307,15 @@ class Io(io.Io):
|
||||||
i2c_address: int,
|
i2c_address: int,
|
||||||
register: int,
|
register: int,
|
||||||
use_cache=False) -> int:
|
use_cache=False) -> int:
|
||||||
|
"""
|
||||||
|
Lese ein Byte Daten von einem I2C-Gerät
|
||||||
|
|
||||||
|
:param i2c_address: I2C-Adresse
|
||||||
|
:param register: I2C-Register
|
||||||
|
:param use_cache: Daten zwischenspeichern und bei erneutem
|
||||||
|
Aufruf nicht erneut abfragen.
|
||||||
|
:return: Datenbyte
|
||||||
|
"""
|
||||||
key = (i2c_address, register)
|
key = (i2c_address, register)
|
||||||
|
|
||||||
if use_cache and key in self.i2c_cache:
|
if use_cache and key in self.i2c_cache:
|
||||||
|
@ -291,19 +329,44 @@ class Io(io.Io):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _i2c_write_byte(self, i2c_address: int, register: int, value: int):
|
def _i2c_write_byte(self, i2c_address: int, register: int, value: int):
|
||||||
|
"""
|
||||||
|
Schreibe ein Byte Daten an ein I2C-Gerät
|
||||||
|
|
||||||
|
:param i2c_address: I2C-Adresse
|
||||||
|
:param register: I2C-Register
|
||||||
|
:param value: Datenbyte
|
||||||
|
"""
|
||||||
self.bus.write_byte_data(i2c_address, register, value)
|
self.bus.write_byte_data(i2c_address, register, value)
|
||||||
|
|
||||||
def _i2c_read_bit(self,
|
def _i2c_read_bit(self,
|
||||||
i2c_address: int,
|
i2c_address: int,
|
||||||
register: int,
|
register: int,
|
||||||
bit: int,
|
bit: int,
|
||||||
use_cache=False):
|
use_cache=False) -> bool:
|
||||||
|
"""
|
||||||
|
Lese ein Bit Daten von einem I2C-Gerät
|
||||||
|
|
||||||
|
:param i2c_address: I2C-Adresse
|
||||||
|
:param register: I2C-Register
|
||||||
|
:param bit: Nummer des Bits (0-7)
|
||||||
|
:param use_cache: Daten zwischenspeichern und bei erneutem
|
||||||
|
Aufruf nicht erneut abfragen.
|
||||||
|
:return Datenbit
|
||||||
|
"""
|
||||||
data = self._i2c_read_byte(i2c_address, register, use_cache)
|
data = self._i2c_read_byte(i2c_address, register, use_cache)
|
||||||
bitmask = 1 << bit
|
bitmask = 1 << bit
|
||||||
return bool(data & bitmask)
|
return bool(data & bitmask)
|
||||||
|
|
||||||
def _i2c_write_bit(self, i2c_address: int, register: int, bit: int,
|
def _i2c_write_bit(self, i2c_address: int, register: int, bit: int,
|
||||||
value: bool):
|
value: bool):
|
||||||
|
"""
|
||||||
|
Schreibe ein Bit Daten an ein I2C-Gerät
|
||||||
|
|
||||||
|
:param i2c_address: I2C-Adresse
|
||||||
|
:param register: I2C-Register
|
||||||
|
:param bit: Nummer des Bits (0-7)
|
||||||
|
:param value: Datenbit
|
||||||
|
"""
|
||||||
data = self._i2c_read_byte(i2c_address, register)
|
data = self._i2c_read_byte(i2c_address, register)
|
||||||
bitmask = 1 << bit
|
bitmask = 1 << bit
|
||||||
|
|
||||||
|
@ -315,26 +378,48 @@ class Io(io.Io):
|
||||||
self._i2c_write_byte(i2c_address, register, data)
|
self._i2c_write_byte(i2c_address, register, data)
|
||||||
|
|
||||||
def _i2c_clear_cache(self):
|
def _i2c_clear_cache(self):
|
||||||
|
"""
|
||||||
|
Leere den I2C-Cache
|
||||||
|
(verwendet von ``_i2c_read_bit()`` und ``_i2c_read_byte()``).
|
||||||
|
"""
|
||||||
self.i2c_cache = {}
|
self.i2c_cache = {}
|
||||||
|
|
||||||
def _configure_mcp(self, i2c_address: int):
|
def _configure_mcp(self, i2c_address: int):
|
||||||
|
"""Schreibe die initiale Konfiguration an ein MCP23017-Gerät"""
|
||||||
|
# I/O-Modus: Lesend
|
||||||
self._i2c_write_byte(i2c_address, MCP_IODIRA, 0xff)
|
self._i2c_write_byte(i2c_address, MCP_IODIRA, 0xff)
|
||||||
self._i2c_write_byte(i2c_address, MCP_IODIRB, 0xff)
|
self._i2c_write_byte(i2c_address, MCP_IODIRB, 0xff)
|
||||||
|
|
||||||
|
# I/O-Polarität: Active-high
|
||||||
self._i2c_write_byte(i2c_address, MCP_IPOLA, 0)
|
self._i2c_write_byte(i2c_address, MCP_IPOLA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_IPOLB, 0)
|
self._i2c_write_byte(i2c_address, MCP_IPOLB, 0)
|
||||||
|
|
||||||
|
# Interrupt aus
|
||||||
self._i2c_write_byte(i2c_address, MCP_GPINTENA, 0)
|
self._i2c_write_byte(i2c_address, MCP_GPINTENA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_GPINTENB, 0)
|
self._i2c_write_byte(i2c_address, MCP_GPINTENB, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_DEFVALA, 0)
|
self._i2c_write_byte(i2c_address, MCP_DEFVALA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_DEFVALB, 0)
|
self._i2c_write_byte(i2c_address, MCP_DEFVALB, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_INTCONA, 0)
|
self._i2c_write_byte(i2c_address, MCP_INTCONA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_INTCONB, 0)
|
self._i2c_write_byte(i2c_address, MCP_INTCONB, 0)
|
||||||
|
|
||||||
|
# Interrupt-Polarität: Active-high
|
||||||
|
# Interrupt-Ports spiegeln
|
||||||
self._i2c_write_byte(i2c_address, MCP_IOCON, 0b01000010)
|
self._i2c_write_byte(i2c_address, MCP_IOCON, 0b01000010)
|
||||||
|
|
||||||
|
# Pullup aus
|
||||||
self._i2c_write_byte(i2c_address, MCP_GPPUA, 0)
|
self._i2c_write_byte(i2c_address, MCP_GPPUA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_GPPUB, 0)
|
self._i2c_write_byte(i2c_address, MCP_GPPUB, 0)
|
||||||
|
|
||||||
|
# Outputs aus
|
||||||
self._i2c_write_byte(i2c_address, MCP_OLATA, 0)
|
self._i2c_write_byte(i2c_address, MCP_OLATA, 0)
|
||||||
self._i2c_write_byte(i2c_address, MCP_OLATB, 0)
|
self._i2c_write_byte(i2c_address, MCP_OLATB, 0)
|
||||||
|
|
||||||
def _configure_input_device(self, device: _MCP23017Device):
|
def _configure_input_device(self, device: _MCP23017Device):
|
||||||
|
"""
|
||||||
|
Konfiguriere einen MCP-Pin als Eingabegerät
|
||||||
|
|
||||||
|
:param device: Gerätedefinition
|
||||||
|
"""
|
||||||
if device.invert:
|
if device.invert:
|
||||||
self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA),
|
self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA),
|
||||||
device.pin, True)
|
device.pin, True)
|
||||||
|
@ -343,6 +428,11 @@ class Io(io.Io):
|
||||||
device.pin, True)
|
device.pin, True)
|
||||||
|
|
||||||
def _configure_output_device(self, device: _MCP23017Device):
|
def _configure_output_device(self, device: _MCP23017Device):
|
||||||
|
"""
|
||||||
|
Konfiguriere einen MCP-Pin als Ausgabegerät
|
||||||
|
|
||||||
|
:param device: Gerätedefinition
|
||||||
|
"""
|
||||||
if device.invert:
|
if device.invert:
|
||||||
# self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA),
|
# self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA),
|
||||||
# device.pin, True)
|
# device.pin, True)
|
||||||
|
@ -353,6 +443,11 @@ class Io(io.Io):
|
||||||
device.pin, False)
|
device.pin, False)
|
||||||
|
|
||||||
def _read_interrupt(self) -> Optional[str]:
|
def _read_interrupt(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Rufe ab, welches Eingabegerät den Interrupt ausgelöst hat
|
||||||
|
|
||||||
|
:return: Name des Eingabegeräts (oder None)
|
||||||
|
"""
|
||||||
self._i2c_clear_cache()
|
self._i2c_clear_cache()
|
||||||
|
|
||||||
for key, device in self.input_devices.items():
|
for key, device in self.input_devices.items():
|
||||||
|
@ -364,6 +459,11 @@ class Io(io.Io):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _read_inputs(self) -> Dict[str, bool]:
|
def _read_inputs(self) -> Dict[str, bool]:
|
||||||
|
"""
|
||||||
|
Lese die Zustände aller Eingabegeräte aus
|
||||||
|
|
||||||
|
:return: Dict(Gerätename => Zustand)
|
||||||
|
"""
|
||||||
res = {}
|
res = {}
|
||||||
self._i2c_clear_cache()
|
self._i2c_clear_cache()
|
||||||
|
|
||||||
|
@ -375,6 +475,13 @@ class Io(io.Io):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _interrupt_handler(self, int_pin: int): # pylint: disable=unused-argument
|
def _interrupt_handler(self, int_pin: int): # pylint: disable=unused-argument
|
||||||
|
"""
|
||||||
|
Diese Funktion wird von der RPi-GPIO-Library bei einem Interrupt des MCP23017
|
||||||
|
(Zustandswechsel eines Eingabegeräts) aufgerufen.
|
||||||
|
|
||||||
|
Es wird abgefragt, welcher Input den Interrupt ausgelöst hat und der
|
||||||
|
entsprechende Input-Callback ausgelöst.
|
||||||
|
"""
|
||||||
key = self._read_interrupt()
|
key = self._read_interrupt()
|
||||||
if key is None:
|
if key is None:
|
||||||
return
|
return
|
||||||
|
@ -389,16 +496,7 @@ class Io(io.Io):
|
||||||
logging.debug('%s pressed', key)
|
logging.debug('%s pressed', key)
|
||||||
else:
|
else:
|
||||||
logging.debug('%s released', key)
|
logging.debug('%s released', key)
|
||||||
|
self._trigger_cb(key)
|
||||||
if key == 'BT_MODE':
|
|
||||||
self._trigger_cb_mode()
|
|
||||||
elif key.startswith('BT_Z_'):
|
|
||||||
zoneid_str = key[5:]
|
|
||||||
try:
|
|
||||||
zoneid = int(zoneid_str)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
self._trigger_cb_manual(zoneid)
|
|
||||||
|
|
||||||
def write_output(self, key: str, val: bool):
|
def write_output(self, key: str, val: bool):
|
||||||
device = self.output_devices[key]
|
device = self.output_devices[key]
|
||||||
|
|
|
@ -36,18 +36,6 @@ class Job:
|
||||||
befinden.
|
befinden.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
def is_active(self) -> bool:
|
|
||||||
"""
|
|
||||||
Gibt True zurück, wenn der Bewässerungsjob in Zukunft ausgeführt werden wird.
|
|
||||||
"""
|
|
||||||
if not self.enable:
|
|
||||||
return False
|
|
||||||
if self.repeat:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return self.date > util.datetime_now()
|
|
||||||
|
|
||||||
def check(self, date_now: datetime) -> bool:
|
def check(self, date_now: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Gibt True zurück, wenn der Bewässerungsjob in dieser Minute
|
Gibt True zurück, wenn der Bewässerungsjob in dieser Minute
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from tsgrain_controller import models, util
|
from tsgrain_controller import models, util
|
||||||
|
|
||||||
|
@ -20,19 +20,23 @@ class TaskQueue(util.StoppableThread, TaskHolder):
|
||||||
self.tasks: List[models.Task] = []
|
self.tasks: List[models.Task] = []
|
||||||
self.running_task: Optional[models.Task] = None
|
self.running_task: Optional[models.Task] = None
|
||||||
|
|
||||||
def enqueue(self, task: models.Task, exclusive: bool = False) -> bool:
|
def enqueue(self, task: models.Task, queuing: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Fügt der Warteschlange einen neuen Task hinzu. Die Warteschlange
|
Fügt der Warteschlange einen neuen Task hinzu. Die Warteschlange
|
||||||
kann nicht mehrere Tasks der selben Quelle und Zone aufnehmen.
|
kann nicht mehrere Tasks der selben Quelle und Zone aufnehmen.
|
||||||
Kann ein Task nicht aufgenommen werden, wird False zurückgegeben.
|
Kann ein Task nicht aufgenommen werden, wird False zurückgegeben.
|
||||||
|
|
||||||
:param task: Neuer Task
|
:param task: Neuer Task
|
||||||
:param exclusive: Wenn True, verbiete mehrere Tasks von der selben Quelle
|
:param queuing: Füge Task der Warteschlange hinzu, wenn bereits
|
||||||
|
ein anderer Task läuft
|
||||||
:return: True wenn Task erfolgreich hinzugefügt
|
:return: True wenn Task erfolgreich hinzugefügt
|
||||||
"""
|
"""
|
||||||
|
if not queuing and self.running_task is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Abbrechen, wenn bereits ein Task mit gleicher Quelle und Zone existiert
|
||||||
for t in self.tasks:
|
for t in self.tasks:
|
||||||
if t.source == task.source and (exclusive
|
if t.source == task.source and t.zone_id == task.zone_id:
|
||||||
or t.zone_id == task.zone_id):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
|
@ -70,10 +74,6 @@ class TaskQueue(util.StoppableThread, TaskHolder):
|
||||||
"""Task zur Speicherung in der Datenbank in dict umwandeln."""
|
"""Task zur Speicherung in der Datenbank in dict umwandeln."""
|
||||||
return self.tasks
|
return self.tasks
|
||||||
|
|
||||||
def serialize_rpc(self) -> Dict[str, Any]:
|
|
||||||
"""Task zur aktuellen Statusübertragung in dict umwandeln."""
|
|
||||||
return {'current_time': util.datetime_now(), 'tasks': self.serialize()}
|
|
||||||
|
|
||||||
def run_cycle(self):
|
def run_cycle(self):
|
||||||
# Get a new task if none is running
|
# Get a new task if none is running
|
||||||
if self.running_task is None:
|
if self.running_task is None:
|
||||||
|
|
Loading…
Reference in a new issue