Initial commit - Support for Get Hydrometer and Get Fermentation Chamber

This commit is contained in:
Jesper Fussing Mørk 2025-10-09 16:27:33 +02:00
commit df1be4c144
27 changed files with 1794 additions and 0 deletions

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#t
Pipfile.lock

16
Pipfile Normal file
View File

@ -0,0 +1,16 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
requests = "*"
pydantic = "*"
loguru = "*"
[dev-packages]
pytest = "*"
responses = "*"
[requires]
python_version = "3.13"

0
README.md Normal file
View File

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
pythonpath = src
testpaths = tests
addopts = -s

10
src/live_test_client.py Normal file
View File

@ -0,0 +1,10 @@
from rapt.client import Client
from rapt.hydrometer import Hydrometer
client = Client("jfm@moerks.dk", "9OBkd5yv1xY8")
hydrometer = Hydrometer(client)
#hydrometer.get_hydrometers()
#hydrometer.get_hydrometer("2aa3b02c-78de-4715-8f5b-61bf7c3d1b62")
hydrometer.get_telemetry("2aa3b02c-78de-4715-8f5b-61bf7c3d1b62", "2025-10-07T12:51:07.3321557+00:00", "2025-10-08T12:51:07.3321557+00:00", "")

0
src/rapt/__init__.py Normal file
View File

65
src/rapt/client.py Normal file
View File

@ -0,0 +1,65 @@
import requests
from datetime import datetime, timedelta
from loguru import logger
class ClientException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class HttpClientException(ClientException):
def __init__(self, message, http_code):
self.message = message
self.http_code = http_code
logger.error(str(self))
def __str__(self):
return "["+str(self.http_code)+"] - " + self.message
class Client():
def __init__(self, username, api_key) -> None:
self.host = "https://api.rapt.io"
self.username = username
self.api_key = api_key
self.token = None
self.expires_seconds = None
self.expires = None
def get_jwt_token(self):
url = "https://id.rapt.io/connect/token"
response = requests.post(url, data={
"client_id": "rapt-user",
"grant_type": "password",
"username": self.username,
"password": self.api_key
})
if response.status_code == 200:
response_json = response.json()
self.token = response_json["access_token"]
self.expires_seconds = response_json["expires_in"]
self.expires = datetime.now() + timedelta(seconds=self.expires_seconds)
logger.trace("Expires: " + str(self.expires))
else:
raise HttpClientException(response.reason, response.status_code)
def get_auth_headers(self):
#TODO: Check expiry
if self.expires is None or self.expires <= datetime.now():
self.get_jwt_token()
return {"Authorization": "Bearer {}".format(self.token)}
def get_json(self, url, parameters):
headers = self.get_auth_headers()
response = requests.get(self.host+url, headers=headers, params=parameters)
logger.trace(response.request.url)
if response.status_code == 200:
return response.json()
else:
raise HttpClientException(response.reason, response.status_code)

View File

@ -0,0 +1,53 @@
from rapt.client import ClientException
from rapt.model.fermentation_chamber import FermentationChamberModel, FermentationChamberTelemetryModel
from typing import Optional, List
from pydantic import TypeAdapter
from loguru import logger
class FermentationChamber():
def __init__(self, client) -> None:
self.client = client
def get_fermentation_chambers(self) -> Optional[List[FermentationChamberModel]]:
logger.debug("## GET FERMENTATION CHAMBERS ##")
try:
response_json = self.client.get_json("/api/fermentationchambers/getfermentationchambers", None)
logger.trace(response_json)
fermentation_chambers_adapter = TypeAdapter(list[FermentationChamberModel])
return fermentation_chambers_adapter.validate_python(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None
def get_fermentation_chamber(self, fermentation_chamber_id) -> Optional[FermentationChamberModel]:
logger.debug("## GET FERMENTATION CHAMBER ##")
params = {
"fermentationChamberId": fermentation_chamber_id
}
try:
response_json = self.client.get_json("/api/fermentationchambers/getfermentationchamber", params)
logger.trace(response_json)
return FermentationChamberModel.model_validate(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None
def get_telemetry(self, fermentation_chamber_id, start_date, end_date, profile_session_id) -> Optional[List[FermentationChamberTelemetryModel]]:
logger.debug("## GET TELEMETRY ##")
params = {
"fermentationChamberId": fermentation_chamber_id,
"startDate": start_date,
"endDate": end_date,
"profileSessionId": profile_session_id
}
try:
response_json = self.client.get_json("/api/fermentationchambers/gettelemetry", params)
logger.trace(response_json)
fermentation_chambers_adapter = TypeAdapter(list[FermentationChamberTelemetryModel])
return fermentation_chambers_adapter.validate_python(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None

53
src/rapt/hydrometer.py Normal file
View File

@ -0,0 +1,53 @@
from rapt.client import ClientException
from rapt.model.hydrometer import HydrometerModel, HydrometerTelemetryModel
from pydantic import TypeAdapter
from typing import Optional, List
from loguru import logger
class Hydrometer():
def __init__(self, client) -> None:
self.client = client
def get_hydrometers(self) -> Optional[List[HydrometerModel]]:
logger.debug("## GET HYDROMETERS ##")
try:
response_json = self.client.get_json("/api/hydrometers/gethydrometers", None)
logger.trace(response_json)
hydrometers_adapter = TypeAdapter(list[HydrometerModel])
return hydrometers_adapter.validate_python(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None
def get_hydrometer(self, hydrometer_id) -> Optional[HydrometerModel]:
logger.debug("## GET HYDROMETER ##")
params = {
"hydrometerId": hydrometer_id
}
try:
response_json = self.client.get_json("/api/hydrometers/gethydrometer", params)
logger.trace(response_json)
return HydrometerModel.model_validate(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None
def get_telemetry(self, hydrometer_id, start_date, end_date, profile_session_id) -> Optional[List[HydrometerTelemetryModel]]:
logger.debug("## GET TELEMETRY ##")
params = {
"hydrometerId": hydrometer_id,
"startDate": start_date,
"endDate": end_date,
"profileSessionId": profile_session_id
}
try:
response_json = self.client.get_json("/api/hydrometers/gettelemetry", params)
logger.trace(response_json)
hydrometers_adapter = TypeAdapter(list[HydrometerTelemetryModel])
return hydrometers_adapter.validate_python(response_json)
except ClientException:
#TODO: Handle Exception gracefully
return None

View File

6
src/rapt/model/amount.py Normal file
View File

@ -0,0 +1,6 @@
from enum import Enum
class AmountTypes(str, Enum):
Weight = "Weight",
Volume = "Volume",
Units = "Units"

13
src/rapt/model/base.py Normal file
View File

@ -0,0 +1,13 @@
from pydantic import BaseModel, Field
from datetime import datetime
from uuid import UUID
class RaptBaseModel(BaseModel):
id: UUID
deleted: bool
created_on: datetime = Field(alias="createdOn")
created_by: UUID = Field(alias="createdBy")
modified_on: datetime = Field(alias="modifiedOn")
modified_by: UUID = Field(alias="modifiedBy")

39
src/rapt/model/device.py Normal file
View File

@ -0,0 +1,39 @@
from enum import Enum
class DeviceTypes(str, Enum):
FermentationChamber = "FermentationChamber",
TemperatureController = "TemperatureController",
Hydrometer = "Hydrometer",
GrainWeigh = "GrainWeigh",
BrewZilla = "BrewZilla",
CanFiller = "CanFiller",
GlycolChiller = "GlycolChiller",
Still = "Still",
BLETemperature = "BLETemperature",
BLEHumidity = "BLEHumidity",
BLETempHumidity = "BLETempHumidity",
BLEPressure = "BLEPressure",
External = "External",
Fridge = "Fridge",
GrainWeighDevice = "GrainWeighDevice",
Unknown = "Unknown"
class DeviceTypesNullable(str, Enum):
FermentationChamber = "FermentationChamber",
TemperatureController = "TemperatureController",
Hydrometer = "Hydrometer",
GrainWeigh = "GrainWeigh",
BrewZilla = "BrewZilla",
CanFiller = "CanFiller",
GlycolChiller = "GlycolChiller",
Still = "Still",
BLETemperature = "BLETemperature",
BLEHumidity = "BLEHumidity",
BLETempHumidity = "BLETempHumidity",
BLEPressure = "BLEPressure",
External = "External",
Fridge = "Fridge",
GrainWeighDevice = "GrainWeighDevice",
Unknown = "Unknown"
null = "null"

View File

@ -0,0 +1,102 @@
from rapt.model.base import RaptBaseModel
from rapt.model.profile import ProfileSessionModel, ProfileSessionStatusModel
from rapt.model.device import DeviceTypes
from rapt.model.toggle import ToggleStates
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from typing import Optional, List
class FermentationChamberTelemetryModel(BaseModel):
id: UUID
row_key: Optional[str] = Field(alias="rowKey", default=None)
created_on: datetime = Field(alias="createdOn")
mac_address: Optional[str] = Field(alias="macAddress", default=None)
rssi: float
control_device_type: Optional[str] = Field(alias="controlDeviceType", default=None)
control_device_mac_address: Optional[str] = Field(alias="controlDeviceMacAddress", default=None)
control_device_temperature: Optional[float] = Field(alias="controlDeviceTemperature", default=None)
temperature: float
target_temperature: Optional[float] = Field(alias="targetTemperature", default=None)
min_target_temperature: Optional[float] = Field(alias="minTargetTemperature", default=None)
max_target_temperature: Optional[float] = Field(alias="maxTargetTemperature", default=None)
total_run_time: float = Field(alias="totalRunTime")
compressor_run_time: float = Field(alias="compressorRunTime")
compressor_starts: float = Field(alias="compressorStarts")
heating_run_time: float = Field(alias="heatingRunTime")
heating_starts: float = Field(alias="heatingStarts")
auxillary_run_time: float = Field(alias="auxillaryRunTime")
auxillary_starts: float = Field(alias="auxillaryStarts")
profile_id: Optional[UUID] = Field(alias="profileId", default=None)
profile_step_id: Optional[UUID] = Field(alias="profileStepId", default=None)
profile_session_start_date: Optional[datetime] = Field(alias="profileSessionStartDate", default=None)
profile_session_time: Optional[int] = Field(alias="profileSessionTime", default=None)
profile_step_progress: Optional[int] = Field(alias="profileStepProgress", default=None)
class FermentationChamberModel(RaptBaseModel):
name: str
serial_number: Optional[str] = Field(alias="serialNumber", default=None)
mac_address: str = Field(alias="macAddress")
device_type: DeviceTypes = Field(alias="deviceType")
active: bool
disabled: bool
username: Optional[str] = None
connection_state: Optional[str] = Field(alias="connectionState", default=None)
status: Optional[str] = None
error: Optional[str] = None
last_activity_time: Optional[datetime] = Field(alias="lastActivityTime", default=None)
rssi: float
firmware_version: Optional[str] = Field(alias="firmwareVersion", default=None)
is_latest_firmware: bool = Field(alias="isLatestFirmware")
active_profile_id: Optional[UUID] = Field(alias="activeProfileId", default=None)
active_profile_step_id: Optional[UUID] = Field(alias="activeProfileStepId", default=None)
active_profile_session: ProfileSessionStatusModel = Field(alias="activeProfileSession")
profile_sessions: Optional[List[ProfileSessionModel]] = Field(alias="profileSessions", default=None)
beta_updates: bool = Field(alias="betaUpdates")
bluetooth_enabled: bool = Field(alias="bluetoothEnabled")
graph_zoom_level: float = Field(alias="graphZoomLevel")
temperature: float
target_temperature: Optional[float] = Field(alias="targetTemperature", default=None)
min_target_temperature: Optional[float] = Field(alias="minTargetTemperature", default=None)
max_target_temperature: Optional[float] = Field(alias="maxTargetTemperature", default=None)
total_run_time: float = Field(alias="totalRunTime")
cooling_enabled: bool = Field(alias="coolingEnabled")
cooling_run_time: float = Field(alias="coolingRunTime")
cooling_starts: float = Field(alias="coolingStarts")
heating_enabled: bool = Field(alias="heatingEnabled")
heating_run_time: float = Field(alias="heatingRunTime")
heating_starts: float = Field(alias="heatingStarts")
heating_utilisation: float = Field(alias="heatingUtilisation")
high_temp_alarm: float = Field(alias="highTempAlarm")
low_temp_alarm: float = Field(alias="lowTempAlarm")
ntc_beta: float = Field(alias="ntcBeta")
ntc_ref_resistance: float = Field(alias="ntcRefResistance")
ntc_ref_temperature: float = Field(alias="ntcRefTemperature")
pid_cycle_time: float = Field(alias="pidCycleTime")
pid_enabled: bool = Field(alias="pidEnabled")
pid_proportional: float = Field(alias="pidProportional")
pid_integral: float = Field(alias="pidIntegral")
pid_derivative: float = Field(alias="pidDerivative")
sensor_differential: float = Field(alias="sensorDifferential")
sensor_timeout: float = Field(alias="sensorTimeout")
show_graph: bool = Field(alias="showGraph")
sounds_enabled: bool = Field(alias="soundsEnabled")
temp_unit: Optional[str] = Field(alias="tempUnit", default=None)
use_internal_sensor: bool = Field(alias="useInternalSensor")
control_device_type: Optional[str] = Field(alias="controlDeviceType", default=None)
control_device_mac_address: Optional[str] = Field(alias="controlDeviceMacAddress", default=None)
control_device_temperature: Optional[float] = Field(alias="controlDeviceTemperature", default=None)
customer_use: str = Field(alias="customerUse")
telemetry_frequency: int = Field(alias="telemetryFrequency")
compressor_delay: float = Field(alias="compressorDelay")
mode_switch_delay: float = Field(alias="modeSwitchDelay")
cooling_hysteresis: float = Field(alias="coolingHysteresis")
heating_hysteresis: float = Field(alias="heatingHysteresis")
telemetry: Optional[List[FermentationChamberTelemetryModel]] = None
compressor_run_time: float = Field(alias="compressorRunTime")
compressor_starts: float = Field(alias="compressorStarts")
auxillary_run_time: float = Field(alias="auxillaryRunTime")
auxillary_starts: float = Field(alias="auxillaryStarts")
fan_enabled: bool = Field(alias="fanEnabled")
light_enabled: ToggleStates = Field(alias="lightEnabled")

View File

@ -0,0 +1,46 @@
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from typing import Optional, List
from rapt.model.base import RaptBaseModel
from rapt.model.device import DeviceTypes, DeviceTypesNullable
from rapt.model.profile import ProfileSessionStatusModel
class HydrometerTelemetryModel(BaseModel):
id: UUID
row_key: Optional[str] = Field(alias="rowKey")
created_on: datetime = Field(alias="createdOn")
mac_address: Optional[str] = Field(alias="macAddress")
rssi: float
temperature: float
gravity: float
gravity_velocity: float = Field(alias="gravityVelocity")
battery: float
version: Optional[str]
class HydrometerModel(RaptBaseModel):
name: str
serial_number: Optional[str] = Field(alias="serialNumber", default=None)
mac_address: str = Field(alias="macAddress")
device_type: DeviceTypes = Field(alias="deviceType")
active: bool
disabled: bool
username: Optional[str] = None
connection_state: Optional[str] = Field(alias="connectionState", default=None)
status: Optional[str] = None
error: Optional[str] = None
last_activity_time: Optional[datetime] = Field(alias="lastActivityTime")
rssi: float
firmware_version: Optional[str] = Field(alias="firmwareVersion")
is_latest_firmware: bool = Field(alias="isLatestFirmware")
active_profile_id: Optional[UUID] = Field(alias="activeProfileId", default=None)
active_profile_step: Optional[UUID] = Field(alias="activeProfileStepId", default=None)
active_profile_session: Optional[ProfileSessionStatusModel] = Field(alias="activeProfileSession", default=None)
telemetry: Optional[List[HydrometerTelemetryModel]]
paired_device_type: Optional[DeviceTypesNullable] = Field(alias="pairedDeviceType", default=None)
paired_device_id: Optional[UUID] = Field(alias="pairedDeviceId", default=None)
temperature: float
gravity: float
gravity_velocity: float = Field(alias="gravityVelocity")
battery: float

134
src/rapt/model/profile.py Normal file
View File

@ -0,0 +1,134 @@
from pydantic import Field
from typing import Optional, List
from enum import Enum
from datetime import datetime
from uuid import UUID
from rapt.model.base import RaptBaseModel
from rapt.model.yeast import YeastModel
class ProfileAlertTriggers(str, Enum):
StepStart = "StepStart",
StepEnd = "StepEnd",
Temperature = "Temperature",
Gravity = "Gravity",
GravityVelocity = "GravityVelocity",
Duration = "Duration"
class ProfileValueOperatorsNullable(str, Enum):
Equals = "Equals",
LessThan = "LessThan",
GreaterThan = "GreaterThan",
null = "null"
class ProfileStepControlTypes(str, Enum):
Target = "Target",
Ramp = "Ramp",
FreeRise = "FreeRise"
class ProfileStepEndTypes(str, Enum):
Duration = "Duration",
Temperature = "Temperature",
Gravity = "Gravity",
GravityVelocity = "GravityVelocity",
Manual = "Manual"
class ProfileStepDurationTypesNullable(str, Enum):
Start = "Start",
Temperature = "Temperature",
Gravity = "Gravity",
GravityVelocity = "GravityVelocity",
Manual = "Manual",
null = "null"
class ProfileAlertModel(RaptBaseModel):
alert_text: str = Field(alias="alertText")
trigger: ProfileAlertTriggers
operator: ProfileValueOperatorsNullable
temperature: float
gravity: float
profile_id: UUID = Field(alias="profileId")
class ProfileStepAlertModel(RaptBaseModel):
alert_text: Optional[str] = Field(alias="alertText")
trigger: ProfileAlertTriggers
operator: Optional[ProfileValueOperatorsNullable]
temperature: Optional[float]
gravity: Optional[float]
length: Optional[int]
profile_step_id: UUID = Field(alias="profileStepId")
class ProfileStepModel(RaptBaseModel):
name: Optional[str]
order: int
control_type: ProfileStepControlTypes = Field(alias="controlType")
end_type: ProfileStepEndTypes = Field(alias="endType")
duration_type: Optional[ProfileStepDurationTypesNullable] = Field(alias="durationType")
operator: Optional[ProfileValueOperatorsNullable]
length: Optional[int]
temperature: Optional[float]
min_temperature: Optional[float] = Field(alias="minTemperature")
max_temperature: Optional[float] = Field(alias="maxTemperature")
gravity: Optional[float]
pump_enabled: Optional[bool] = Field(alias="pumpEnabled")
pump_utilisation: Optional[float] = Field(alias="pumpUtilisation")
heating_utilisation: Optional[float] = Field(alias="heatingUtilisation")
pid_enabled: Optional[bool] = Field(alias="pidEnabled")
sensor_differential: Optional[float] = Field(alias="sensorDifferential")
profile_id: UUID = Field(alias="profileId")
alerts: Optional[List[ProfileStepAlertModel]]
class ProfileSessionModel(RaptBaseModel):
name: Optional[str]
description: Optional[str]
profile_id: Optional[UUID] = Field(alias="profileId")
# profile: ProfileModel
brewzilla_id: Optional[UUID] = Field(alias="brewZillaId")
fermentation_chamger_id: Optional[UUID] = Field(alias="fermentationChamberId")
hydrometer_id: Optional[UUID] = Field(alias="hydrometerId")
still_id: Optional[UUID] = Field(alias="stillId")
temperature_controller_id: Optional[UUID] = Field(alias="temperatureControllerId")
start_date: Optional[datetime] = Field(alias="startDate")
end_date: Optional[datetime] = Field(alias="endDate")
original_gravity: Optional[float] = Field(alias="originalGravity")
final_gravity: Optional[float] = Field(alias="finalGravity")
yeast_id: Optional[UUID] = Field(alias="yeastId")
yeast: YeastModel
sent_alerts: List[Optional[ProfileAlertModel]] = Field(alias="sentAlerts")
class ProfileModel(RaptBaseModel):
name: str
description: Optional[str]
public: bool
profile_name: Optional[str] = Field(alias="profileName")
rating: float
rating_count: int = Field(alias="ratingCount")
rating_score: float = Field(alias="ratingScore")
copy_count: float = Field(alias="copyCount")
view_count: float = Field(alias="viewCount")
profile_type_id: Optional[UUID] = Field(alias="profileTypeId")
alerts: Optional[List[ProfileAlertModel]]
steps: Optional[List[ProfileStepModel]]
profile_sessions: Optional[List[ProfileSessionModel]] = Field(alias="profileSessions")
class ProfileSessionStatusModel(RaptBaseModel):
name: Optional[str]
description: Optional[str]
profile_id: Optional[UUID] = Field(alias="profileId")
profile: ProfileModel
brewzilla_id: Optional[UUID] = Field(alias="brewZillaId")
fermentation_chamger_id: Optional[UUID] = Field(alias="fermentationChamberId")
hydrometer_id: Optional[UUID] = Field(alias="hydrometerId")
still_id: Optional[UUID] = Field(alias="stillId")
temperature_controller_id: Optional[UUID] = Field(alias="temperatureControllerId")
start_date: Optional[datetime] = Field(alias="startDate")
end_date: Optional[datetime] = Field(alias="endDate")
original_gravity: Optional[float] = Field(alias="originalGravity")
final_gravity: Optional[float] = Field(alias="finalGravity")
yeast_id: Optional[UUID] = Field(alias="yeastId")
yeast: YeastModel
sent_alerts: List[Optional[ProfileAlertModel]] = Field(alias="sentAlerts")
estimated_end_date: Optional[datetime] = Field(alias="estimatedEndDate")
profile_length: Optional[float] = Field(alias="profileLength")
current_profile_time: Optional[float] = Field(alias="currentProfileTime")
remaining_profile_time: Optional[float] = Field(alias="remainingProfileTime")

6
src/rapt/model/toggle.py Normal file
View File

@ -0,0 +1,6 @@
from enum import Enum
class ToggleStates(str, Enum):
AlwaysOn = "AlwaysOn",
AlwaysOff = "AlwaysOff",
Automatic = "Automatic"

29
src/rapt/model/yeast.py Normal file
View File

@ -0,0 +1,29 @@
from pydantic import Field
from typing import Optional
from uuid import UUID
from rapt.model.base import RaptBaseModel
from rapt.model.amount import AmountTypes
class YeastModel(RaptBaseModel):
name: Optional[str]
product_id: Optional[str] = Field(alias="productId")
laboratory: Optional[str]
supplier: Optional[str]
kegland_product_code: Optional[str] = Field(alias="keglandProductCode")
yeast_type: Optional[str] = Field(alias="type")
form: Optional[str]
min_temperature: float = Field(alias="minTemperature")
max_temperature: float = Field(alias="maxTemperature")
flocculation: Optional[str]
attenuation: float
notes: Optional[str]
best_for: Optional[str] = Field(alias="bestFor")
max_reuse: int = Field(alias="maxReuse")
add_to_secondary: bool = Field(alias="addToSecondary")
amount_types: AmountTypes = Field(alias="amountType")
inventory_amount: Optional[float] = Field(alias="inventoryAmount")
archived: bool
stock_level: float = Field(alias="stockLevel")
global_yeast_id: Optional[UUID] = Field(alias="globalYeastId")
is_global: bool = Field(alias="isGlobal")

View File

@ -0,0 +1,384 @@
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"serialNumber": "string",
"macAddress": "string",
"deviceType": "FermentationChamber",
"active": true,
"disabled": true,
"username": "string",
"connectionState": "string",
"status": "string",
"error": "string",
"lastActivityTime": "2025-10-09T11:56:29.302Z",
"rssi": 0,
"firmwareVersion": "string",
"isLatestFirmware": true,
"activeProfileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"activeProfileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"activeProfileSession": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"public": true,
"profileName": "string",
"rating": 0,
"ratingCount": 0,
"ratingScore": 0,
"copyCount": 0,
"viewCount": 0,
"profileTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
],
"steps": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"order": 0,
"controlType": "Target",
"endType": "Duration",
"durationType": "Start",
"operator": "Equals",
"length": 0,
"temperature": 0,
"minTemperature": 0,
"maxTemperature": 0,
"gravity": 0,
"pumpEnabled": true,
"pumpUtilisation": 0,
"heatingUtilisation": 0,
"pidEnabled": true,
"sensorDifferential": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"length": 0,
"profileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
],
"profileSessions": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": "string",
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:56:29.302Z",
"endDate": "2025-10-09T11:56:29.302Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
]
},
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:56:29.302Z",
"endDate": "2025-10-09T11:56:29.302Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
],
"estimatedEndDate": "2025-10-09T11:56:29.302Z",
"profileLength": 0,
"currentProfileTime": 0,
"remainingProfileTime": 0
},
"profileSessions": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": "string",
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:56:29.302Z",
"endDate": "2025-10-09T11:56:29.302Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:56:29.302Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:56:29.302Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
],
"betaUpdates": true,
"bluetoothEnabled": true,
"graphZoomLevel": 0,
"temperature": 0,
"targetTemperature": 0,
"minTargetTemperature": 0,
"maxTargetTemperature": 0,
"totalRunTime": 0,
"coolingEnabled": true,
"coolingRunTime": 0,
"coolingStarts": 0,
"heatingEnabled": true,
"heatingRunTime": 0,
"heatingStarts": 0,
"heatingUtilisation": 0,
"highTempAlarm": 0,
"lowTempAlarm": 0,
"ntcBeta": 0,
"ntcRefResistance": 0,
"ntcRefTemperature": 0,
"pidCycleTime": 0,
"pidEnabled": true,
"pidProportional": 0,
"pidIntegral": 0,
"pidDerivative": 0,
"sensorDifferential": 0,
"sensorTimeout": 0,
"showGraph": true,
"soundsEnabled": true,
"tempUnit": "string",
"useInternalSensor": true,
"controlDeviceType": "string",
"controlDeviceMacAddress": "string",
"controlDeviceTemperature": 0,
"customerUse": "string",
"telemetryFrequency": 14440,
"compressorDelay": 10,
"modeSwitchDelay": 30,
"coolingHysteresis": 10,
"heatingHysteresis": 10,
"telemetry": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"rowKey": "string",
"createdOn": "2025-10-09T11:56:29.302Z",
"macAddress": "string",
"rssi": 0,
"controlDeviceType": "string",
"controlDeviceMacAddress": "string",
"controlDeviceTemperature": 0,
"temperature": 0,
"targetTemperature": 0,
"minTargetTemperature": 0,
"maxTargetTemperature": 0,
"totalRunTime": 0,
"compressorRunTime": 0,
"compressorStarts": 0,
"heatingRunTime": 0,
"heatingStarts": 0,
"auxillaryRunTime": 0,
"auxillaryStarts": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileSessionStartDate": "2025-10-09T11:56:29.302Z",
"profileSessionTime": 0,
"profileStepProgress": 0
}
],
"compressorRunTime": 0,
"compressorStarts": 0,
"auxillaryRunTime": 0,
"auxillaryStarts": 0,
"fanEnabled": true,
"lightEnabled": "AlwaysOn"
}

View File

@ -0,0 +1,28 @@
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"rowKey": "string",
"createdOn": "2025-10-09T09:29:04.638Z",
"macAddress": "string",
"rssi": 0,
"controlDeviceType": "string",
"controlDeviceMacAddress": "string",
"controlDeviceTemperature": 0,
"temperature": 0,
"targetTemperature": 0,
"minTargetTemperature": 0,
"maxTargetTemperature": 0,
"totalRunTime": 0,
"compressorRunTime": 0,
"compressorStarts": 0,
"heatingRunTime": 0,
"heatingStarts": 0,
"auxillaryRunTime": 0,
"auxillaryStarts": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileSessionStartDate": "2025-10-09T09:29:04.638Z",
"profileSessionTime": 0,
"profileStepProgress": 0
}
]

View File

@ -0,0 +1,386 @@
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"serialNumber": "string",
"macAddress": "string",
"deviceType": "FermentationChamber",
"active": true,
"disabled": true,
"username": "string",
"connectionState": "string",
"status": "string",
"error": "string",
"lastActivityTime": "2025-10-09T11:13:05.885Z",
"rssi": 0,
"firmwareVersion": "string",
"isLatestFirmware": true,
"activeProfileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"activeProfileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"activeProfileSession": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"public": true,
"profileName": "string",
"rating": 0,
"ratingCount": 0,
"ratingScore": 0,
"copyCount": 0,
"viewCount": 0,
"profileTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
],
"steps": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"order": 0,
"controlType": "Target",
"endType": "Duration",
"durationType": "Start",
"operator": "Equals",
"length": 0,
"temperature": 0,
"minTemperature": 0,
"maxTemperature": 0,
"gravity": 0,
"pumpEnabled": true,
"pumpUtilisation": 0,
"heatingUtilisation": 0,
"pidEnabled": true,
"sensorDifferential": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"length": 0,
"profileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
],
"profileSessions": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": "string",
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:13:05.885Z",
"endDate": "2025-10-09T11:13:05.885Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
]
},
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:13:05.885Z",
"endDate": "2025-10-09T11:13:05.885Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
],
"estimatedEndDate": "2025-10-09T11:13:05.885Z",
"profileLength": 0,
"currentProfileTime": 0,
"remainingProfileTime": 0
},
"profileSessions": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"description": "string",
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profile": "string",
"brewZillaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fermentationChamberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"hydrometerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"stillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"temperatureControllerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-10-09T11:13:05.885Z",
"endDate": "2025-10-09T11:13:05.885Z",
"originalGravity": 0,
"finalGravity": 0,
"yeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"yeast": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"productId": "string",
"laboratory": "string",
"supplier": "string",
"keglandProductCode": "string",
"type": "string",
"form": "string",
"minTemperature": 0,
"maxTemperature": 0,
"flocculation": "string",
"attenuation": 0,
"notes": "string",
"bestFor": "string",
"maxReuse": 0,
"addToSecondary": true,
"amountType": "Weight",
"inventoryAmount": 0,
"archived": true,
"stockLevel": 0,
"globalYeastId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isGlobal": true
},
"sentAlerts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"deleted": true,
"createdOn": "2025-10-09T11:13:05.885Z",
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"modifiedOn": "2025-10-09T11:13:05.885Z",
"modifiedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"alertText": "string",
"trigger": "StepStart",
"operator": "Equals",
"temperature": 0,
"gravity": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
]
}
],
"betaUpdates": true,
"bluetoothEnabled": true,
"graphZoomLevel": 0,
"temperature": 0,
"targetTemperature": 0,
"minTargetTemperature": 0,
"maxTargetTemperature": 0,
"totalRunTime": 0,
"coolingEnabled": true,
"coolingRunTime": 0,
"coolingStarts": 0,
"heatingEnabled": true,
"heatingRunTime": 0,
"heatingStarts": 0,
"heatingUtilisation": 0,
"highTempAlarm": 0,
"lowTempAlarm": 0,
"ntcBeta": 0,
"ntcRefResistance": 0,
"ntcRefTemperature": 0,
"pidCycleTime": 0,
"pidEnabled": true,
"pidProportional": 0,
"pidIntegral": 0,
"pidDerivative": 0,
"sensorDifferential": 0,
"sensorTimeout": 0,
"showGraph": true,
"soundsEnabled": true,
"tempUnit": "string",
"useInternalSensor": true,
"controlDeviceType": "string",
"controlDeviceMacAddress": "string",
"controlDeviceTemperature": 0,
"customerUse": "string",
"telemetryFrequency": 14440,
"compressorDelay": 10,
"modeSwitchDelay": 30,
"coolingHysteresis": 10,
"heatingHysteresis": 10,
"telemetry": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"rowKey": "string",
"createdOn": "2025-10-09T11:13:05.885Z",
"macAddress": "string",
"rssi": 0,
"controlDeviceType": "string",
"controlDeviceMacAddress": "string",
"controlDeviceTemperature": 0,
"temperature": 0,
"targetTemperature": 0,
"minTargetTemperature": 0,
"maxTargetTemperature": 0,
"totalRunTime": 0,
"compressorRunTime": 0,
"compressorStarts": 0,
"heatingRunTime": 0,
"heatingStarts": 0,
"auxillaryRunTime": 0,
"auxillaryStarts": 0,
"profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"profileSessionStartDate": "2025-10-09T11:13:05.885Z",
"profileSessionTime": 0,
"profileStepProgress": 0
}
],
"compressorRunTime": 0,
"compressorStarts": 0,
"auxillaryRunTime": 0,
"auxillaryStarts": 0,
"fanEnabled": true,
"lightEnabled": "AlwaysOn"
}
]

View File

@ -0,0 +1,35 @@
{
"telemetry": [
{
"temperature": 17.5625,
"gravity": 1325.07,
"gravityVelocity": 0,
"battery": 0,
"version": "20250319_055542_b3788ba",
"id": "68423584-1a35-4341-b67c-78456f4ae9f3",
"rowKey": "2516424593326678442",
"createdOn": "2025-10-07T12:51:07.3321557+00:00",
"macAddress": "ac-15-18-df-84-94",
"rssi": -56
}
],
"temperature": 17.5625,
"gravity": 1325.07,
"gravityVelocity": 0,
"battery": 0,
"name": "Hegnsgården Yellow",
"macAddress": "ac-15-18-df-84-94",
"deviceType": "Hydrometer",
"active": false,
"disabled": false,
"lastActivityTime": "2025-10-07T12:51:07.3321557+00:00",
"rssi": -56,
"firmwareVersion": "20250319_055542_b3788ba",
"isLatestFirmware": true,
"modifiedOn": "2025-10-07T12:51:07.3603727+00:00",
"modifiedBy": "00000000-0000-0000-0000-000000000000",
"id": "2aa3b02c-78de-4715-8f5b-61bf7c3d1b62",
"deleted": false,
"createdOn": "2025-10-07T09:44:01.1515163+00:00",
"createdBy": "475ebc33-8e90-4be5-2424-08ddfbe49482"
}

View File

@ -0,0 +1,14 @@
[
{
"temperature": 17.5625,
"gravity": 1325.07,
"gravityVelocity": 0,
"battery": 0,
"version": "20250319_055542_b3788ba",
"id": "50d5885c-b637-4343-bb47-cc76e9f3c0c1",
"rowKey": "2516424593326678442",
"createdOn": "2025-10-07T12:51:07.3321557+00:00",
"macAddress": "ac-15-18-df-84-94",
"rssi": -56
}
]

View File

@ -0,0 +1,37 @@
[
{
"telemetry": [
{
"temperature": 17.5625,
"gravity": 1325.07,
"gravityVelocity": 0,
"battery": 0,
"version": "20250319_055542_b3788ba",
"id": "55462886-4400-4608-b283-3ce4426dafd3",
"rowKey": "2516424593326678442",
"createdOn": "2025-10-07T12:51:07.3321557+00:00",
"macAddress": "ac-15-18-df-84-94",
"rssi": -56
}
],
"temperature": 17.5625,
"gravity": 1325.07,
"gravityVelocity": 0,
"battery": 0,
"name": "Hegnsgården Yellow",
"macAddress": "ac-15-18-df-84-94",
"deviceType": "Hydrometer",
"active": false,
"disabled": false,
"lastActivityTime": "2025-10-07T12:51:07.3321557+00:00",
"rssi": -56,
"firmwareVersion": "20250319_055542_b3788ba",
"isLatestFirmware": false,
"modifiedOn": "2025-10-07T12:51:07.3603727+00:00",
"modifiedBy": "00000000-0000-0000-0000-000000000000",
"id": "2aa3b02c-78de-4715-8f5b-61bf7c3d1b62",
"deleted": false,
"createdOn": "2025-10-07T09:44:01.1515163+00:00",
"createdBy": "475ebc33-8e90-4be5-2424-08ddfbe49482"
}
]

View File

@ -0,0 +1,6 @@
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkYzM0E0QzYzNkZFOUE3RjBFNzVGQkRBQTQ1REIwOTdBQjUxRUVGMTlSUzI1NiIsIng1dCI6Ijh6cE1ZMl9wcF9Eblg3MnFSZHNKZXJVZTd4ayIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2lkLnJhcHQuaW8iLCJuYmYiOjE3NTk5MjcyMjQsImlhdCI6MTc1OTkyNzIyNCwiZXhwIjoxNzU5OTMwODI0LCJhdWQiOiJyYXB0LWFwaSIsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJyYXB0LWFwaSIsInJhcHQtYXBpLnB1YmxpYyJdLCJhbXIiOlsiY3VzdG9tIl0sImNsaWVudF9pZCI6InJhcHQtdXNlciIsInN1YiI6IjQ3NWViYzMzLThlOTAtNGJlNS0yNDI0LTA4ZGRmYmU0OTQ4MiIsImF1dGhfdGltZSI6MTc1OTkyNzIyNCwiaWRwIjoibG9jYWwiLCJmaXJzdG5hbWUiOiJKZXNwZXIiLCJsYXN0bmFtZSI6IkZ1c3NpbmcgTVx1MDBGOHJrIiwiY291bnRyeSI6IkRlbm1hcmsiLCJlbWFpbCI6ImpmbUBtb2Vya3MuZGsiLCJwcm9maWxlbmFtZSI6ImpmbSIsImVtYWlsY29uZmlybWVkIjoiZmFsc2UiLCJqdGkiOiI3RDM3QjcxMDNBMTc4QkQ0RkMwM0ZCRDNGNTBFMDY2MiJ9.HJtC6Cfps5rqsukMYFRi7S4awpz0KqJjSX1EdL1K6NMpxVEmqmiiA3LwXrsjCbu2FoLjswxyM7rCFqVBZnnQ71DcQJ9gJCe1Ddonj-l5SfA90KHjlvLPQOhvCLrYHc9ulDEi64Dz-WfH8V7WCy1fgl3lY8xmRstS8rjO-5jVwQFFlZvyOHoke09iMsRQmCyuiGYbqmn1i31VSK6L3fMuRfrc1o165T1Xts66vAupJ6nqEhHnAusvCM2gagEvAiXx3hH3xxid4K6zCQLvbg0jxKUHnKI9q2xVj30bW0C2_lKSDKpOOTLjWpjrwjVag5DYxYFggNVgTvF90f_hYddxFX8s9_dgY6EbTunSA1ki86r82Rw7Ebvfwy1phjNHllgu85Y73tIYADcQYxzjglNKPX1Dzed8liUOAXTHgL2Z9-4_q8bwmgNrGy9eH4K_PjyqEuuhDvy2GQxjhOojOXJOU-aSaE4KwaHTod1CZB-kaikMAKjsHDTi3cRspFU4sli665zgPZ2UKYsjTWSlqoKpA7wO3RrMKQiCrNEqyE0ldOi1ctCS-dBEBkuhc-THTwOqMDQTQ6Iqqk1dK72Nm8uB5TwPur-TP4YYxi1hY6w0jSOs0JzUVpaMGgAXg3CM_zhWgTE9S44R3gjxX6EsXayI5f5WtstxpHMbB3rO_DrZPjA",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "openid profile rapt-api rapt-api.public"
}

View File

@ -0,0 +1,83 @@
import pytest
import json
import responses
from rapt.client import Client
from rapt.fermentation_chamber import FermentationChamber
@pytest.fixture
def client():
yield Client("test", "test")
@responses.activate
def test_get_fermentation_chambers(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/fermentationchambers/getfermentationchambers", json=json_reader("./tests/json/get_fermentation_chambers_response.json"))
fermentation_chamber = FermentationChamber(client)
ferms = fermentation_chamber.get_fermentation_chambers()
assert ferms is not None
assert len(ferms) == 1
@responses.activate
def test_get_fermentation_chambers_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/fermentationchambers/getfermentationchambers",
status=500,
)
fermentation_chamber = FermentationChamber(client)
ferms = fermentation_chamber.get_fermentation_chambers()
assert ferms is None
@responses.activate
def test_get_fermentation_chamber(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/fermentationchambers/getfermentationchamber", json=json_reader("./tests/json/get_fermentation_chamber_response.json"))
fermentation_chamber = FermentationChamber(client)
ferms = fermentation_chamber.get_fermentation_chamber("")
assert ferms is not None
@responses.activate
def test_get_fermentation_chamber_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/fermentationchambers/getfermentationchamber",
status=500,
)
fermentation_chamber = FermentationChamber(client)
ferms = fermentation_chamber.get_fermentation_chamber("")
assert ferms is None
@responses.activate
def test_get_telemetry(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/fermentationchambers/gettelemetry", json=json_reader("./tests/json/get_fermentation_chamber_telemetry_response.json"))
fermentation_chamber = FermentationChamber(client)
telemetry = fermentation_chamber.get_telemetry("", "", "", "")
assert telemetry is not None
assert len(telemetry) == 1
@responses.activate
def test_get_telemetry_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/fermentationchambers/gettelemetry",
status=500,
)
fermentation_chamber = FermentationChamber(client)
telemetry = fermentation_chamber.get_telemetry("", "", "", "")
assert telemetry is None
def json_reader(path):
with open(path) as f:
return json.load(f)

83
tests/test_hydrometer.py Normal file
View File

@ -0,0 +1,83 @@
import pytest
import json
import responses
from rapt.client import Client
from rapt.hydrometer import Hydrometer
@pytest.fixture
def client():
yield Client("test", "test")
@responses.activate
def test_get_hydrometers(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/hydrometers/gethydrometers", json=json_reader("./tests/json/get_hydrometers_response.json"))
hydrometer = Hydrometer(client)
hydros = hydrometer.get_hydrometers()
assert hydros is not None
assert len(hydros) == 1
@responses.activate
def test_get_hydrometers_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/hydrometers/gethydrometers",
status=500,
)
hydrometer = Hydrometer(client)
hydros = hydrometer.get_hydrometers()
assert hydros is None
@responses.activate
def test_get_hydrometer(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/hydrometers/gethydrometer", json=json_reader("./tests/json/get_hydrometer_response.json"))
hydrometer = Hydrometer(client)
hydros = hydrometer.get_hydrometer("")
assert hydros is not None
@responses.activate
def test_get_hydrometer_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/hydrometers/gethydrometer",
status=500,
)
hydrometer = Hydrometer(client)
hydros = hydrometer.get_hydrometer("")
assert hydros is None
@responses.activate
def test_get_telemetry(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.get("https://api.rapt.io/api/hydrometers/gettelemetry", json=json_reader("./tests/json/get_hydrometer_telemetry_response.json"))
hydrometer = Hydrometer(client)
telemetry = hydrometer.get_telemetry("", "", "", "")
assert telemetry is not None
@responses.activate
def test_get_telemetry_500(client):
responses.post("https://id.rapt.io/connect/token", json=json_reader("./tests/json/token_response.json"))
responses.add(
responses.GET,
"https://api.rapt.io/api/hydrometers/gettelemetry",
status=500,
)
hydrometer = Hydrometer(client)
telemetry = hydrometer.get_telemetry("", "", "", "")
assert telemetry is None
def json_reader(path):
with open(path) as f:
return json.load(f)