Skip to content
Open
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
50 changes: 50 additions & 0 deletions src/ducopy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,36 @@ def change_action_node(
typer.echo(f"Failed to perform POST action for node {node_id}: {e}")
raise typer.Exit(code=1)

@app.command()
def update_config(
base_url: str,
config_json: Annotated[str, typer.Option(help="Configuration parameters as a JSON string")],
format: Annotated[str, typer.Option(help="Output format: pretty or json")] = "pretty",
) -> None:
"""
Update configuration settings for the box device.

Args:
base_url (str): The base URL of the API.
config_json (str): Configuration parameters as a JSON string.
format (str): Output format: pretty or json.
"""
base_url = validate_url(base_url)
facade = DucoPy(base_url)
try:
config = json.loads(config_json)
#config = ConfigRequest(**config_data)
except (json.JSONDecodeError, ValidationError) as e:
logger.error("Invalid configuration data: {}", e)
typer.echo(f"Invalid configuration data: {e}")
raise typer.Exit(code=1)
try:
response = facade.update_config(config=config)
print_output(response, format)
except Exception as e:
logger.error("Error updating configuration for the box device: {}", str(e))
typer.echo(f"Failed to update configuration for the box device : {e}")
raise typer.Exit(code=1)

@app.command()
def update_config_node(
Expand Down Expand Up @@ -265,6 +295,26 @@ def update_config_node(
typer.echo(f"Failed to update configuration for node {node_id}: {e}")
raise typer.Exit(code=1)

@app.command()
def get_config(
base_url: str, format: Annotated[str, typer.Option(help="Output format: pretty or json")] = "pretty"
) -> None:
"""
Retrieve configuration settings for the box device.

Args:
base_url (str): The base URL of the API.
format (str): Output format: pretty or json.
"""
base_url = validate_url(base_url)
facade = DucoPy(base_url)
try:
response = facade.get_config()
print_output(response, format)
except Exception as e:
logger.error("Error fetching box configuration: {}", str(e))
typer.echo(f"Failed to fetch box configuration: {e}")
raise typer.Exit(code=1)

@app.command()
def get_config_nodes(
Expand Down
19 changes: 19 additions & 0 deletions src/ducopy/ducopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ def change_action_node(self, action: str, value: str, node_id: int) -> ActionsCh
"""
return self.client.post_action_node(action, value, node_id)

def update_config(self, config):
"""Update the configuration for the box device.

Args:
config (Config): The configuration data to update.

Returns:
Config: The updated configuration response from the server.
"""
return self.client.patch_config(config=config)

def update_config_node(self, node_id: int, config: ConfigNodeRequest) -> ConfigNodeResponse:
"""Update the configuration for a specific node.

Expand Down Expand Up @@ -166,6 +177,14 @@ def get_info(self, module: str | None = None, submodule: str | None = None, para
"""
return self.client.get_info(module=module, submodule=submodule, parameter=parameter)

def get_config(self):
"""Retrieve configuration settings for the box device.

Returns:
NodesResponse: Configuration settings for all nodes.
"""
return self.client.get_config()

def get_nodes(self) -> NodesInfoResponse:
"""Retrieve a list of all nodes.

Expand Down
107 changes: 107 additions & 0 deletions src/ducopy/rest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,113 @@ def get_config_nodes(self) -> NodesResponse:
logger.debug("Received configuration data for all nodes")
return NodesResponse(**response.json()) # Parse response into NodesResponse model

def get_config(self):
"""
Retrieve configuration settings for the box device.

Returns:
NodesResponse: Parsed response containing configuration data for all nodes.
"""
endpoint = "/config"
logger.info("Fetching configuration for all the box device: {}", endpoint)
response = self.session.get(endpoint)
response.raise_for_status()
logger.debug("Received configuration data for all nodes")
return response.json()

def validate_config(self, errors, template, request, path):
for field, new_value in request.items():
new_path = f"{path}->{field}"
old_value = template.get(field)
# check whether the new field exists in the old configuration
if old_value is None:
error_message = f"Parameter '{new_path}' not available for box."
logger.error(error_message)
errors.append(error_message)
continue
# the new field exists in the old configuration
if type(new_value) is dict:
# recurse
self.validate_config(errors, old_value, new_value, new_path)
else:
# it is a leaf node
val = old_value.get("Val")
if val is None:
error_message = f"Parameter '{new_path}' not settable for box."
logger.error(error_message)
errors.append(error_message)
continue

# an actual value to be set
min_val = old_value.get("Min")
max_val = old_value.get("Max")
inc = old_value.get("Inc")
options = old_value.get("Options")

# Check if new_value is within Min and Max
if min_val is not None and new_value < min_val:
error_message = f"Value {new_value} for '{new_path}' is less than minimum {min_val}."
logger.error(error_message)
errors.append(error_message)
if max_val is not None and new_value > max_val:
error_message = f"Value {new_value} for '{new_path}' is greater than maximum {max_val}."
logger.error(error_message)
errors.append(error_message)

# Check if new_value aligns with increment
if inc is not None:
base_value = min_val if min_val is not None else 0
if (new_value - base_value) % inc != 0:
error_message = (
f"Value {new_value} for '{new_path}' is not a valid increment of {inc} starting from {base_value}."
)
logger.error(error_message)
errors.append(error_message)

# Check if listed in the valid options
if options is not None:
if new_value not in options:
error_message = f"Value {new_value} for '{new_path}' is not in {options}."
logger.error(error_message)
errors.append(error_message)

# we are good to go!
request[field] = {"Val": new_value}

def patch_config(self, config):
"""
Update configuration settings for the box as a whole after validating the new values.

Args:
config (Config): The configuration data to update.

Returns:
Config: The updated configuration response from the server.
"""
logger.info("Updating configuration for box as a whole")

# Fetch current configuration of the node
current_config = self.get_config()

# Validation logic (same as before)
validation_errors = []
self.validate_config(validation_errors, current_config, config, "")

if validation_errors:
# Raise an exception with all validation errors
raise ValueError("Validation errors:\n" + "\n".join(validation_errors))

# Send PATCH request if validation passes
endpoint = f"/config"
logger.info("Sending PATCH request with body: {}", config)
response = self.session.patch(endpoint, data=json.dumps(config, separators=(',', ':')))

#response = self.session.patch(endpoint, json=request_body)
response.raise_for_status()
logger.debug("Updated configuration for box as a whole")

return self.get_config()

def get_api_info(self) -> dict:
"""Fetch API version and available endpoints."""
logger.info("Fetching API information")
Expand Down