Initial commit - Support for Get Hydrometer and Get Fermentation Chamber

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

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")