Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
WS281xLedConfig,
)
from src.config.errors import ConfigError
from src.config.validators import build_number_limiter, validate_max_length, valide_no_identical_active_i2c_devices
from src.config.validators import build_distinct_validator, build_number_limiter, validate_max_length
from src.filepath import CUSTOM_CONFIG_FILE
from src.logger_handler import LoggerHandler
from src.models import CocktailStatus
Expand Down Expand Up @@ -191,26 +191,29 @@ def __init__(self) -> None:
DictType(
{
"pin_type": ChooseOptions.pin,
"board_number": IntType([build_number_limiter(1, 99)], prefix="#", default=1),
"pin": IntType([build_number_limiter(0)], prefix="Pin:"),
"volume_flow": FloatType([build_number_limiter(0.1, 1000)], suffix="ml/s"),
"tube_volume": IntType([build_number_limiter(0, 100)], suffix="ml"),
},
PumpConfig,
),
lambda: self.choose_bottle_number(ignore_limits=True),
[build_distinct_validator(["pin_type", "board_number", "pin"])],
),
"I2C_CONFIG": ListType(
DictType(
{
"device_type": ChooseOptions.i2c,
"board_number": IntType([build_number_limiter(1, 99)], prefix="#", default=1),
"enabled": BoolType(check_name="Enabled", default=True),
"address_int": IntType(prefix="0x", default=20),
"inverted": BoolType(check_name="Inverted"),
},
I2CExpanderConfig,
),
0,
[valide_no_identical_active_i2c_devices],
[build_distinct_validator(["device_type", "board_number"])],
),
"MAKER_NAME": StringType([validate_max_length]),
"MAKER_NUMBER_BOTTLES": IntType([build_number_limiter(1, 999)]),
Expand All @@ -221,8 +224,9 @@ def __init__(self) -> None:
"MAKER_PUMP_REVERSION_CONFIG": DictType(
{
"use_reversion": BoolType(check_name="active", default=False),
"pin": IntType([build_number_limiter(0)], default=0, prefix="Pin:"),
"pin_type": ChooseOptions.pin,
"board_number": IntType([build_number_limiter(1, 99)], prefix="#", default=1),
"pin": IntType([build_number_limiter(0)], default=0, prefix="Pin:"),
"inverted": BoolType(check_name="Inverted", default=False),
},
ReversionConfig,
Expand All @@ -239,6 +243,7 @@ def __init__(self) -> None:
DictType(
{
"pin_type": ChooseOptions.pin,
"board_number": IntType([build_number_limiter(1, 99)], prefix="#", default=1),
"pin": IntType([build_number_limiter(0)], prefix="Pin:"),
"default_on": BoolType(check_name="Default On"),
"preparation_state": ChooseOptions.leds,
Expand Down Expand Up @@ -415,21 +420,22 @@ def _validate_cross_config(self, validate: bool) -> None:
- Checks that WAITER_MODE is not enabled alongside CocktailBerry NFC payment.
- Check that some nfc is active when payment/waiter mode is active.
"""
# Collect all I2C pin types used in PUMP_CONFIG (excluding GPIO)
required_i2c_types: set[str] = set()
# Collect all I2C (pin_type, board_number) pairs used in PUMP_CONFIG (excluding GPIO)
required_i2c_boards: set[tuple[str, int]] = set()
for pump in self.PUMP_CONFIG:
if pump.pin_type != "GPIO":
required_i2c_types.add(pump.pin_type)
required_i2c_boards.add((pump.pin_type, pump.board_number))

# Get enabled I2C device types from I2C_CONFIG
enabled_i2c_types = {cfg.device_type for cfg in self.I2C_CONFIG if cfg.enabled}
# Get enabled I2C (device_type, board_number) pairs from I2C_CONFIG
enabled_i2c_boards = {(cfg.device_type, cfg.board_number) for cfg in self.I2C_CONFIG if cfg.enabled}

# Check that all required I2C types have enabled devices
missing_types = required_i2c_types - enabled_i2c_types
if missing_types:
# Check that all required I2C boards have enabled devices
missing_boards = required_i2c_boards - enabled_i2c_boards
if missing_boards:
readable = ", ".join(f"{t} board {n}" for t, n in missing_boards)
error_msg = (
f"PUMP_CONFIG uses I2C pin types {', '.join(missing_types)} but I2C_CONFIG "
"has no enabled devices of these types. Add enabled entries to I2C_CONFIG."
f"PUMP_CONFIG uses I2C boards {readable} but I2C_CONFIG "
"has no enabled devices for them. Add enabled entries to I2C_CONFIG."
)
_logger.error(f"Config Error: {error_msg}")
if validate:
Expand Down
34 changes: 24 additions & 10 deletions src/config/config_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,16 +318,17 @@ def get_default_config_class(self) -> ConfigClassT:

@dataclass(frozen=True)
class PinId:
"""Unique identifier for a pin, combining the pin type and pin number."""
"""Unique identifier for a pin, combining the pin type, board number, and pin number."""

pin_type: SupportedPinControlType
board_number: int
pin: int

def __str__(self) -> str:
return f"{self.pin_type}-{self.pin}"
return f"{self.pin_type}-{self.board_number}-{self.pin}"

def __repr__(self) -> str:
return f"{self.pin_type}-{self.pin}"
return f"{self.pin_type}-{self.board_number}-{self.pin}"


class PumpConfig(ConfigClass):
Expand All @@ -339,20 +340,23 @@ def __init__(
volume_flow: float,
tube_volume: int,
pin_type: SupportedPinControlType = "GPIO",
board_number: int = 1,
) -> None:
self.pin_type = pin_type
self.pin = pin
self.volume_flow = volume_flow
self.tube_volume = tube_volume
self.board_number = board_number

@property
def pin_id(self) -> PinId:
"""Build PinId from this config's pin_type and pin."""
return PinId(self.pin_type, self.pin)
"""Build PinId from this config's pin_type, board_number and pin."""
return PinId(self.pin_type, self.board_number, self.pin)

def to_config(self) -> dict[str, int | float | SupportedPinControlType]:
return {
"pin_type": self.pin_type,
"board_number": self.board_number,
"pin": self.pin,
"volume_flow": self.volume_flow,
"tube_volume": self.tube_volume,
Expand All @@ -364,6 +368,7 @@ class I2CExpanderConfig(ConfigClass):

Shared by MCP23017 (16 pins, 0-15), PCF8574 (8 pins, 0-7), and PCA9535 (16 pins, 0-15).
Default I2C address is 0x20, configurable to 0x20-0x27.
Multiple boards of the same type are distinguished by board_number.
"""

device_type: I2CExpanderType
Expand All @@ -374,11 +379,13 @@ def __init__(
enabled: bool,
address_int: int,
inverted: bool,
board_number: int = 1,
) -> None:
self.device_type = device_type
self.enabled = enabled
self.address_int = address_int
self.inverted = inverted
self.board_number = board_number

@property
def address_hex(self) -> int:
Expand All @@ -387,6 +394,7 @@ def address_hex(self) -> int:
def to_config(self) -> dict[str, bool | int | str]:
return {
"device_type": self.device_type,
"board_number": self.board_number,
"enabled": self.enabled,
"address_int": self.address_int,
"inverted": self.inverted,
Expand All @@ -404,20 +412,23 @@ def __init__(
pin: int,
inverted: bool,
pin_type: SupportedPinControlType = "GPIO",
board_number: int = 1,
) -> None:
self.use_reversion = use_reversion
self.pin = pin
self.pin_type = pin_type
self.inverted = inverted
self.board_number = board_number

@property
def pin_id(self) -> PinId:
"""Build PinId from this config's pin_type and pin."""
return PinId(self.pin_type, self.pin)
"""Build PinId from this config's pin_type, board_number and pin."""
return PinId(self.pin_type, self.board_number, self.pin)

def to_config(self) -> dict[str, Any]:
return {
"pin_type": self.pin_type,
"board_number": self.board_number,
"pin": self.pin,
"use_reversion": self.use_reversion,
"inverted": self.inverted,
Expand All @@ -435,20 +446,23 @@ def __init__(
default_on: bool,
preparation_state: SupportedLedStatesType,
pin_type: SupportedPinControlType = "GPIO",
board_number: int = 1,
) -> None:
self.pin = pin
self.default_on = default_on
self.preparation_state = preparation_state
self.pin_type = pin_type
self.board_number = board_number

@property
def pin_id(self) -> PinId:
"""Build PinId from this config's pin_type and pin."""
return PinId(self.pin_type, self.pin)
"""Build PinId from this config's pin_type, board_number and pin."""
return PinId(self.pin_type, self.board_number, self.pin)

def to_config(self) -> dict[str, Any]:
return {
"pin_type": self.pin_type,
"board_number": self.board_number,
"pin": self.pin,
"default_on": self.default_on,
"preparation_state": self.preparation_state,
Expand Down Expand Up @@ -477,7 +491,7 @@ def __init__(
@property
def pin_id(self) -> PinId:
"""Build PinId from this config's pin_type and pin."""
return PinId("GPIO", self.pin)
return PinId("GPIO", 1, self.pin)

def to_config(self) -> dict[str, Any]:
return {
Expand Down
28 changes: 19 additions & 9 deletions src/config/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@ def limit_number(configname: str, data: float) -> None:
return limit_number


def valide_no_identical_active_i2c_devices(configname: str, data: list[dict]) -> None:
"""Validate that no I2C device type is enabled more than once."""
active_types = [d["device_type"] for d in data if d["enabled"]]
duplicates = {t for t in active_types if active_types.count(t) > 1}
if duplicates:
raise ConfigError(
f"{configname} has duplicate enabled device types: "
f"{', '.join(duplicates)}. Each type can only be enabled once."
)
def build_distinct_validator(keys: list[str]) -> Callable[[str, list[dict]], None]:
"""Build a validator that checks list items have distinct values across the given keys.

Each combination of values for the specified keys must be unique across all items in the list.
"""

def validate_distinct(configname: str, data: list[dict]) -> None:
seen: list[tuple] = []
for item in data:
key_tuple = tuple(item.get(k) for k in keys)
if key_tuple in seen:
readable = ", ".join(f"{k}={item.get(k)}" for k in keys)
raise ConfigError(
f"{configname} has duplicate entries for ({readable}). "
f"Each combination of {', '.join(keys)} must be unique."
)
seen.append(key_tuple)

return validate_distinct
16 changes: 8 additions & 8 deletions src/language.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -724,11 +724,11 @@ ui:
en: 'Disable all other tabs than the maker one; You will only be prompted at program start if you want to go to config window with master password'
de: 'Alle anderen Tabs als den Maker-Tab deaktivieren; Du wirst nur beim Programmstart gefragt, ob du mit dem Master-Passwort zum Konfigurationsfenster gehen möchtest'
PUMP_CONFIG:
en: 'Setting for each pump: Pin connected to the pump, volume flow in ml/s, tube volume to the pump in ml (used to pump given volume after a bottle change). I2C start at 0-7 or 0-15. If using I2C, the pin is the pin on the I2C relais (0-7 or 0-15) and not the GPIO pin.'
de: 'Einstellung für jede Pumpe: Pin verbunden mit der Pumpe, Volumenstrom in ml/s, Schlauchvolumen zur Pumpe in ml (wird verwendet, um das gegebene Volumen nach einem Flaschenwechsel zu pumpen). I2C beginnt bei 0-7 oder 0-15. Bei Verwendung von I2C ist der Pin der Pin auf dem I2C-Relais (0-7 oder 0-15) und nicht der GPIO-Pin.'
en: 'Setting for each pump: Pin type (GPIO or I2C board), board number (# only relevant for I2C, to distinguish multiple boards of the same type), pin connected to the pump, volume flow in ml/s, tube volume to the pump in ml (used to pump given volume after a bottle change). I2C pins start at 0-7 or 0-15 depending on the board type.'
de: 'Einstellung für jede Pumpe: Pin-Typ (GPIO oder I2C-Board), Board-Nummer (# nur relevant für I2C, um mehrere Boards des gleichen Typs zu unterscheiden), Pin verbunden mit der Pumpe, Volumenstrom in ml/s, Schlauchvolumen zur Pumpe in ml (wird verwendet, um das gegebene Volumen nach einem Flaschenwechsel zu pumpen). I2C-Pins beginnen bei 0-7 oder 0-15 je nach Board-Typ.'
I2C_CONFIG:
en: 'I2C GPIO expander configuration. Enable if using I2C Relais for pump control. Set the I2C address (default 0x20). See supported boards over dropdown.'
de: 'I2C GPIO-Expander Konfiguration. Aktivieren wenn I2C Relais für Pumpensteuerung verwendet wird. I2C-Adresse setzen (Standard 0x20). Unterstützte Boards über Dropdown auswählen.'
en: 'I2C GPIO expander configuration. Select the board type, board number (# to distinguish multiple boards of the same type), enable/disable, I2C address (default 0x20) and signal inversion. Multiple boards of the same type are supported with different board numbers.'
de: 'I2C GPIO-Expander Konfiguration. Board-Typ, Board-Nummer (# um mehrere Boards des gleichen Typs zu unterscheiden), Aktivierung, I2C-Adresse (Standard 0x20) und Signalinvertierung auswählen. Mehrere Boards des gleichen Typs werden über verschiedene Board-Nummern unterstützt.'
MAKER_NAME:
en: 'Name of your CocktailBerry machine'
de: 'Name von deiner CocktailBerry Maschine'
Expand All @@ -748,8 +748,8 @@ ui:
en: 'Value to scale alcoholic ingredients in each recipe. Use this to decrease/increase the alcohol level of all cocktails. Default is 100%'
de: 'Skalierungsfaktor für alkoholische Zutaten in jedem Rezept. Verwende diesen, um den Alkoholgehalt aller Cocktails zu verringern/erhöhen. Standard ist 100%'
MAKER_PUMP_REVERSION_CONFIG:
en: 'Configuration for pump reversion, needed for some setups to be able to revert the pump direction.'
de: 'Konfiguration für Pumpen Umkehrung, benötigt von einigen Setups, um die Pumpenrichtung ändern zu können.'
en: 'Configuration for pump reversion: pin type, board number (# only relevant for I2C), pin and signal inversion. Needed for some setups to be able to revert the pump direction.'
de: 'Konfiguration für Pumpen Umkehrung: Pin-Typ, Board-Nummer (# nur relevant für I2C), Pin und Signalinvertierung. Benötigt von einigen Setups, um die Pumpenrichtung ändern zu können.'
MAKER_SEARCH_UPDATES:
en: 'Search for updates, if available internet connection'
de: 'Sucht nach Updates, wenn Internetverbindung besteht'
Expand Down Expand Up @@ -778,8 +778,8 @@ ui:
en: 'Adds a random cocktail tile to the maker tab that picks a surprise cocktail on prepare'
de: 'Fügt eine Zufalls-Cocktail Kachel zum Maker Tab hinzu, die beim Zubereiten einen Überraschungscocktail wählt'
LED_NORMAL:
en: 'List with config for each normal (non-addressable) LED: pin, default on state, preparation state (on/off/effects)'
de: 'Liste mit Einstellung für jeden normalen (nicht ansteuerbaren) LED: Pin, Standard an-Zustand, Zustand bei Zubereitung (an/aus/Effekte)'
en: 'List with config for each normal (non-addressable) LED: pin type, board number (# only relevant for I2C), pin, default on state, preparation state (on/off/effects)'
de: 'Liste mit Einstellung für jeden normalen (nicht ansteuerbaren) LED: Pin-Typ, Board-Nummer (# nur relevant für I2C), Pin, Standard an-Zustand, Zustand bei Zubereitung (an/aus/Effekte)'
LED_WSLED:
en: 'List with config for each WS281x (addressable) LED: pin, brightness, LED count, number of daisy chained rings, default on state, preparation state (on/off/effects). Only supports GPIO.'
de: 'Liste mit Einstellung für jeden WS281x (ansteuerbaren) LED: Pin, Helligkeit, LED Anzahl, Anzahl der in Serie verbundenen Ringe, Standard an-Zustand, Zustand bei Zubereitung (an/aus/Effekte). Unterstützt nur GPIO.'
Expand Down
Loading