From 01aa870c55c86b2aba75770639049ff76447edf3 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Mon, 7 Feb 2022 14:20:45 +0100 Subject: [PATCH 1/5] log application start/stop --- tsgrain_controller/__main__.py | 1 + tsgrain_controller/application.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tsgrain_controller/__main__.py b/tsgrain_controller/__main__.py index 778f082..db0f4d2 100644 --- a/tsgrain_controller/__main__.py +++ b/tsgrain_controller/__main__.py @@ -1,3 +1,4 @@ +import logging import signal import sys import os diff --git a/tsgrain_controller/application.py b/tsgrain_controller/application.py index c30bc45..316a340 100644 --- a/tsgrain_controller/application.py +++ b/tsgrain_controller/application.py @@ -171,6 +171,7 @@ class Application(models.AppInterface): systimecfg.set_system_timezone(tz, self.cfg.cmd_set_timezone) def start(self): + logging.info('Starting application') self._running = True self.io.start() self.outputs.start() @@ -179,6 +180,7 @@ class Application(models.AppInterface): self.grpc_server.start() def stop(self): + logging.info('Stopping application') self._running = False self.grpc_server.stop(None) self.scheduler.stop() From 83439ecfa41e3c47ae3092c73b7b4e34e36455ce Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Tue, 8 Feb 2022 00:40:46 +0100 Subject: [PATCH 2/5] fix task queue when auto mode is interrupted --- requirements_dev.txt | 1 - tests/test_queue.py | 27 +++++++ tsgrain_controller/__main__.py | 1 - tsgrain_controller/application.py | 2 +- tsgrain_controller/io/__init__.py | 12 ++- tsgrain_controller/io/mcp23017.py | 117 +++++++++++++++++++++++++++++- tsgrain_controller/task_queue.py | 12 ++- 7 files changed, 158 insertions(+), 14 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 0a98eec..b28ee75 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,4 +2,3 @@ grpcio-tools~=1.43.0 mypy-protobuf~=3.2.0 types-protobuf~=3.19.6 grpc-stubs~=1.24.7 -bump2version diff --git a/tests/test_queue.py b/tests/test_queue.py index dd339e8..88ad0bc 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -80,3 +80,30 @@ def test_queue_runner(): assert util.to_json(q) == \ '[{"source": "MANUAL", "zone_id": 2, "duration": 5, "remaining": 4}, \ {"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() diff --git a/tsgrain_controller/__main__.py b/tsgrain_controller/__main__.py index db0f4d2..778f082 100644 --- a/tsgrain_controller/__main__.py +++ b/tsgrain_controller/__main__.py @@ -1,4 +1,3 @@ -import logging import signal import sys import os diff --git a/tsgrain_controller/application.py b/tsgrain_controller/application.py index 316a340..7313127 100644 --- a/tsgrain_controller/application.py +++ b/tsgrain_controller/application.py @@ -106,7 +106,7 @@ class Application(models.AppInterface): task = models.Task(request.source, request.zone_id, duration) task.validate(self) - started = self.queue.enqueue(task, not request.queuing) + started = self.queue.enqueue(task, request.queuing) return models.TaskRequestResult(started, False) def start_task(self, source: models.Source, zone_id: int, duration: int, diff --git a/tsgrain_controller/io/__init__.py b/tsgrain_controller/io/__init__.py index 54b913b..a471aa1 100644 --- a/tsgrain_controller/io/__init__.py +++ b/tsgrain_controller/io/__init__.py @@ -10,6 +10,7 @@ class Io: def set_callbacks(self, cb_manual: Optional[Callable[[int], None]], cb_mode: Optional[Callable[[], None]]): + """Setze Callback-Funktionen für die Eingabegeräte""" self.cb_manual = cb_manual self.cb_mode = cb_mode @@ -22,10 +23,15 @@ class Io: self.cb_mode() def start(self): - pass + """Initialisiere die IO""" def stop(self): - pass + """Beende die IO und deaktiviere alle Ausgabegeräte""" 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 + """ diff --git a/tsgrain_controller/io/mcp23017.py b/tsgrain_controller/io/mcp23017.py index 6f0f207..25846b6 100644 --- a/tsgrain_controller/io/mcp23017.py +++ b/tsgrain_controller/io/mcp23017.py @@ -38,7 +38,8 @@ Polarität pro GPIOA-Pin. - ``0`` High wenn aktiv 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 @@ -197,6 +198,8 @@ class PinConfigInvalid(Exception): class _MCP23017Port(Enum): + """IO-Port des MCP23017 (A/B)""" + A = 0 B = 1 @@ -206,13 +209,41 @@ class _MCP23017Port(Enum): @dataclass class _MCP23017Device: + """ + Ein einzelnes mit einen MCP23017 I2C-Portexpander verbundenes + Eingabe/Ausgabegerät. + """ + i2c_address: int + """I2C-Adresse des MCP23017""" + port: _MCP23017Port + """IO-Port des MCP23017 (A/B)""" + pin: int + """IO-Pin des MCP23017 (0-7)""" + invert: bool + """Zustand des Pins invertieren""" @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('/') if len(cfg_parts) < 2: raise PinConfigInvalid(cfg_str) @@ -263,7 +294,7 @@ class Io(io.Io): self.output_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(): device = _MCP23017Device.from_config(cfg_str) self.mcp_addresses.add(device.i2c_address) @@ -278,6 +309,15 @@ class Io(io.Io): i2c_address: int, register: 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) if use_cache and key in self.i2c_cache: @@ -291,19 +331,44 @@ class Io(io.Io): return data 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) def _i2c_read_bit(self, i2c_address: int, register: 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) bitmask = 1 << bit return bool(data & bitmask) def _i2c_write_bit(self, i2c_address: int, register: int, bit: int, 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) bitmask = 1 << bit @@ -315,26 +380,48 @@ class Io(io.Io): self._i2c_write_byte(i2c_address, register, data) def _i2c_clear_cache(self): + """ + Leere den I2C-Cache + (verwendet von ``_i2c_read_bit()`` und ``_i2c_read_byte()``). + """ self.i2c_cache = {} 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_IODIRB, 0xff) + + # I/O-Polarität: Active-high self._i2c_write_byte(i2c_address, MCP_IPOLA, 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_GPINTENB, 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_INTCONA, 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) + + # Pullup aus self._i2c_write_byte(i2c_address, MCP_GPPUA, 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_OLATB, 0) def _configure_input_device(self, device: _MCP23017Device): + """ + Konfiguriere einen MCP-Pin als Eingabegerät + + :param device: Gerätedefinition + """ if device.invert: self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA), device.pin, True) @@ -343,6 +430,11 @@ class Io(io.Io): device.pin, True) def _configure_output_device(self, device: _MCP23017Device): + """ + Konfiguriere einen MCP-Pin als Ausgabegerät + + :param device: Gerätedefinition + """ if device.invert: # self._i2c_write_bit(device.i2c_address, device.port.reg(MCP_IPOLA), # device.pin, True) @@ -353,6 +445,11 @@ class Io(io.Io): device.pin, False) 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() for key, device in self.input_devices.items(): @@ -364,6 +461,11 @@ class Io(io.Io): return None def _read_inputs(self) -> Dict[str, bool]: + """ + Lese die Zustände aller Eingabegeräte aus + + :return: Dict(Gerätename => Zustand) + """ res = {} self._i2c_clear_cache() @@ -375,6 +477,13 @@ class Io(io.Io): return res 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() if key is None: return diff --git a/tsgrain_controller/task_queue.py b/tsgrain_controller/task_queue.py index 37e1d0f..da3981c 100644 --- a/tsgrain_controller/task_queue.py +++ b/tsgrain_controller/task_queue.py @@ -20,19 +20,23 @@ class TaskQueue(util.StoppableThread, TaskHolder): self.tasks: List[models.Task] = [] 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 kann nicht mehrere Tasks der selben Quelle und Zone aufnehmen. Kann ein Task nicht aufgenommen werden, wird False zurückgegeben. :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 """ + 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: - if t.source == task.source and (exclusive - or t.zone_id == task.zone_id): + if t.source == task.source and t.zone_id == task.zone_id: return False self.tasks.append(task) From 87336262d8a8a037e35c02ef40a7d465fb838085 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Tue, 8 Feb 2022 01:06:29 +0100 Subject: [PATCH 3/5] refactored input callbacks --- tests/test_systimecfg.py | 12 +++++++++++- tsgrain_controller/application.py | 27 ++++++++++++++++----------- tsgrain_controller/io/__init__.py | 29 ++++++++++++++++------------- tsgrain_controller/io/console.py | 4 ++-- tsgrain_controller/io/mcp23017.py | 15 ++------------- tsgrain_controller/task_queue.py | 6 +----- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/tests/test_systimecfg.py b/tests/test_systimecfg.py index 7e974b3..5bf1e68 100644 --- a/tests/test_systimecfg.py +++ b/tests/test_systimecfg.py @@ -20,7 +20,7 @@ def test_run_cmd(cmd_str: str, raise_err: bool): systimecfg._run_cmd(cmd_str) -def get_system_timezone(mocker): +def test_get_system_timezone(mocker): mock_res = mock.Mock() mock_res.stdout = 'Europe/Berlin' @@ -38,6 +38,16 @@ def get_system_timezone(mocker): 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): cmd_run_mock: mock.MagicMock = mocker.patch('subprocess.run') diff --git a/tsgrain_controller/application.py b/tsgrain_controller/application.py index 7313127..9719bbf 100644 --- a/tsgrain_controller/application.py +++ b/tsgrain_controller/application.py @@ -39,7 +39,7 @@ class Application(models.AppInterface): self.db.load_queue(self.queue) 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) @@ -71,16 +71,21 @@ class Application(models.AppInterface): def get_logger(self) -> logging.Logger: return self.logger - def _cb_manual(self, zone_id: int): - self.request_task( - models.TaskRequest(source=models.Source.MANUAL, - zone_id=zone_id, - duration=self.cfg.manual_time, - queuing=False, - cancelling=True)) - - def _cb_modekey(self): - self.set_auto_mode(not self.get_auto_mode()) + def _input_cb(self, key: str): + if key == 'BT_MODE': + self.set_auto_mode(not self.get_auto_mode()) + elif key.startswith('BT_Z_'): + zoneid_str = key[5:] + try: + zone_id = int(zoneid_str) + except ValueError: + return + 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, request: models.TaskRequest) -> models.TaskRequestResult: diff --git a/tsgrain_controller/io/__init__.py b/tsgrain_controller/io/__init__.py index a471aa1..5299c15 100644 --- a/tsgrain_controller/io/__init__.py +++ b/tsgrain_controller/io/__init__.py @@ -5,22 +5,25 @@ from typing import Callable, Optional class Io: def __init__(self, *args): # pylint: disable=unused-argument - self.cb_manual: Optional[Callable[[int], None]] = None - self.cb_mode: Optional[Callable[[], None]] = None + self.cb_input: Optional[Callable[[str], None]] = None - def set_callbacks(self, cb_manual: Optional[Callable[[int], None]], - cb_mode: Optional[Callable[[], None]]): - """Setze Callback-Funktionen für die Eingabegeräte""" - self.cb_manual = cb_manual - self.cb_mode = cb_mode + def set_callback(self, cb: Optional[Callable[[str], None]]): + """ + Setze die Callback-Funktion, die bei einer Eingabe aufgerufen wird. + Als Parameter wird der Name des Eingabegeräts mit übergeben. - def _trigger_cb_manual(self, zone_id: int): - if self.cb_manual is not None: - self.cb_manual(zone_id) + :param cb: Input-Callback-Funktion + """ + self.cb_input = cb - def _trigger_cb_mode(self): - if self.cb_mode is not None: - self.cb_mode() + def _trigger_cb(self, key: str): + """ + 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): """Initialisiere die IO""" diff --git a/tsgrain_controller/io/console.py b/tsgrain_controller/io/console.py index e1b1b12..2cc1bba 100644 --- a/tsgrain_controller/io/console.py +++ b/tsgrain_controller/io/console.py @@ -77,10 +77,10 @@ class Io(util.StoppableThread, io.Io): # Mode key (0) if c == 48: - self._trigger_cb_mode() + self._trigger_cb('BT_MODE') # Zone keys (1-7) elif 49 <= c <= 55: - self._trigger_cb_manual(c - 48) + self._trigger_cb(f'BT_Z_{c - 48}') self._screen.erase() self._screen.addstr(0, 0, diff --git a/tsgrain_controller/io/mcp23017.py b/tsgrain_controller/io/mcp23017.py index 25846b6..61d9b53 100644 --- a/tsgrain_controller/io/mcp23017.py +++ b/tsgrain_controller/io/mcp23017.py @@ -3,7 +3,7 @@ import logging import time from dataclasses import dataclass 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 import smbus @@ -282,8 +282,6 @@ class Io(io.Io): def __init__(self, app: models.AppInterface): super().__init__() - self.cb_manual: Optional[Callable[[int], None]] = None - self.cb_mode: Optional[Callable[[], None]] = None self.cfg = app.get_cfg() self.bus = smbus.SMBus(self.cfg.i2c_bus_id) @@ -498,16 +496,7 @@ class Io(io.Io): logging.debug('%s pressed', key) else: logging.debug('%s released', 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) + self._trigger_cb(key) def write_output(self, key: str, val: bool): device = self.output_devices[key] diff --git a/tsgrain_controller/task_queue.py b/tsgrain_controller/task_queue.py index da3981c..6b50f76 100644 --- a/tsgrain_controller/task_queue.py +++ b/tsgrain_controller/task_queue.py @@ -1,6 +1,6 @@ # coding=utf-8 import logging -from typing import Any, Dict, List, Optional +from typing import List, Optional from tsgrain_controller import models, util @@ -74,10 +74,6 @@ class TaskQueue(util.StoppableThread, TaskHolder): """Task zur Speicherung in der Datenbank in dict umwandeln.""" 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): # Get a new task if none is running if self.running_task is None: From 3644a0c441fb59d6c93877cf617911ce801ea395 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Tue, 8 Feb 2022 01:23:41 +0100 Subject: [PATCH 4/5] add tests for jobschedule --- tests/test_application.py | 14 ++++++++++++++ tests/test_jobschedule.py | 22 ++++++++++++++++++++++ tsgrain_controller/models.py | 12 ------------ 3 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 tests/test_jobschedule.py diff --git a/tests/test_application.py b/tests/test_application.py index ba3947d..2286230 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -120,6 +120,20 @@ def test_start_task_queue(app): assert tasks[0].zone_id == 2 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): # Insert jobs diff --git a/tests/test_jobschedule.py b/tests/test_jobschedule.py new file mode 100644 index 0000000..69dbc35 --- /dev/null +++ b/tests/test_jobschedule.py @@ -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 diff --git a/tsgrain_controller/models.py b/tsgrain_controller/models.py index 6fdd15c..360f47a 100644 --- a/tsgrain_controller/models.py +++ b/tsgrain_controller/models.py @@ -36,18 +36,6 @@ class Job: 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: """ Gibt True zurück, wenn der Bewässerungsjob in dieser Minute From 4b971c01a6e8840484797a52f0336cbfaff544f3 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Wed, 9 Feb 2022 05:44:25 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Bump=20version:=200.1.3=20=E2=86=92=200.1.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- setup.py | 2 +- tsgrain_controller/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d9286d4..6672172 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.3 +current_version = 0.1.4 commit = True tag = True diff --git a/setup.py b/setup.py index 3db03fc..6924516 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open('README.rst') as f: setuptools.setup( name='TSGRain Controller', - version='0.1.3', + version='0.1.4', author='ThetaDev', description='TSGRain irrigation controller', long_description=README, diff --git a/tsgrain_controller/__init__.py b/tsgrain_controller/__init__.py index 8ce9b36..7525d19 100644 --- a/tsgrain_controller/__init__.py +++ b/tsgrain_controller/__init__.py @@ -1 +1 @@ -__version__ = '0.1.3' +__version__ = '0.1.4'