From a0d8443de24d78999d7ab3fc43a9365aa52f625c Mon Sep 17 00:00:00 2001 From: Thomas Hahn Date: Sun, 16 Jun 2024 08:25:49 +0200 Subject: [PATCH] Formatted output in cli --- data/output-format.json | 122 ++++++++++++++++++ src/homematicip/cli/helper.py | 6 +- src/homematicip/cli/hmip.py | 45 ++++--- .../cli/output_formatter/__init__.py | 0 .../cli/output_formatter/formatter.py | 26 ++++ src/homematicip/configuration/config.py | 5 +- src/homematicip/configuration/config_io.py | 42 +++--- .../configuration/output_format_config.py | 10 ++ 8 files changed, 215 insertions(+), 41 deletions(-) create mode 100644 data/output-format.json create mode 100644 src/homematicip/cli/output_formatter/__init__.py create mode 100644 src/homematicip/cli/output_formatter/formatter.py create mode 100644 src/homematicip/configuration/output_format_config.py diff --git a/data/output-format.json b/data/output-format.json new file mode 100644 index 00000000..0efbb907 --- /dev/null +++ b/data/output-format.json @@ -0,0 +1,122 @@ +{ + "functional_channel_attributes": [ + "accelerationSensorEventFilterPeriod", + "accelerationSensorMode", + "accelerationSensorNeutralPosition", + "accelerationSensorSensitivity", + "accelerationSensorTriggerAngle", + "accelerationSensorTriggered", + "actualTemperature", + "alarmContactType", + "authorized", + "averageIllumination", + "binaryBehaviorType", + "blindModeActive", + "colorTemperature", + "currentIllumination", + "currentPowerConsumption", + "dimLevel", + "display", + "doorHandleType", + "doorLockDirection", + "doorState", + "energyCounter", + "genericAlarmSignal", + "highestIllumination", + "humidity", + "illumination", + "impulseDuration", + "leftCounter", + "leftRightCounterDelta", + "lowBat", + "lowestIllumination", + "moistureDetected", + "motionBufferActive", + "motionDetected", + "motorState", + "multiModeInputMode", + "notificationSoundTypeHighToLow", + "notificationSoundTypeLowToHigh", + "on", + "passageBlindtime", + "passageDirection", + "passageSensorSensitivity", + "powerMainsFailure", + "previousShutterLevel", + "previousSlatsLevel", + "primaryShadingLevel", + "processing", + "profileMode", + "pumpFollowUpTime", + "pumpLeadTime", + "pumpProtectionDuration", + "pumpProtectionSwitchingInterval", + "raining", + "rightCounter", + "rssiDeviceValue", + "rssiPeerValue", + "saturationLevel", + "secondaryShadingLevel", + "selfCalibrationInProgress", + "setPointTemperature", + "shutterLevel", + "simpleRGBColorState", + "slatsLevel", + "smokeDetectorAlarmType", + "storm", + "sunshine", + "temperatureExternalDelta", + "temperatureExternalOne", + "temperatureExternalTwo", + "temperatureOffset", + "todayRainCounter", + "todaySunshineDuration", + "totalRainCounter", + "totalSunshineDuration", + "unreach", + "valveActualTemperature", + "valvePosition", + "valveState", + "vaporAmount", + "waterlevelDetected", + "windSpeed", + "windowState", + "yesterdayRainCounter", + "yesterdaySunshineDuration" + ], + "group_attributes": [ + "lowBat", + "unreach", + "dutyCycle", + "incorrectPositioned", + "activeProfile", + "actualTemperature", + "boostDuration", + "boostMode", + "humidity", + "partyMode", + "valvePosition", + "windowState", + "motionDetected", + "presenceDetected", + "on", + "triggered", + "primaryShadingLevel", + "secondaryShadingLevel", + "shutterLevel", + "slatsLevel", + "moistureDetected", + "waterLevelDetected", + "signalAcoustic", + "signalOptical", + "illumination", + "enabled", + "ventilationLevel" + ], + "device_attributes": [ + "connectionType", + "modelType", + "permanentlyReachable", + "externalService" + ] +} \ No newline at end of file diff --git a/src/homematicip/cli/helper.py b/src/homematicip/cli/helper.py index 8847b4d5..961bd5f8 100644 --- a/src/homematicip/cli/helper.py +++ b/src/homematicip/cli/helper.py @@ -31,13 +31,13 @@ async def get_initialized_runner(including_model: bool = True) -> Runner: def get_config() -> Config: """Get Config object from the configuration file. If no configuration file is found, raise an exception.""" - config = ConfigIO.find_config_in_well_known_locations() + persistent_config = ConfigIO.find_config_in_well_known_locations() - if config is None: + if persistent_config is None: raise ClickException( "No configuration file found. Run hmip auth to get an auth token.") - return Config.from_persistent_config(config) + return Config.from_persistent_config(persistent_config) def setup_basic_logging(log_level: int, logger_filename: str = None) -> None: diff --git a/src/homematicip/cli/hmip.py b/src/homematicip/cli/hmip.py index 7daee809..6b2f3799 100644 --- a/src/homematicip/cli/hmip.py +++ b/src/homematicip/cli/hmip.py @@ -23,6 +23,9 @@ is_device, get_device_or_group, is_group, get_channel_name, get_device_name, get_group_name from homematicip.cli.helper import get_rssi_bar_string from homematicip.cli.hmip_cli_show_targets_helper import build_commands_from_registry, CommandEntry +from homematicip.cli.output_formatter.formatter import generate_output_functional_channel, generate_output_group, \ + generate_output_device +from homematicip.configuration import config_io from homematicip.configuration.config import PersistentConfig from homematicip.configuration.config_folder import get_default_app_config_folder from homematicip.configuration.config_io import ConfigIO @@ -142,12 +145,17 @@ def devices(): """List all devices including the functional channels.""" runner = asyncio.run(get_initialized_runner()) model = runner.model + format_config = ConfigIO.get_output_format_config() print("Devices:") for device in sorted(model.devices.values(), key=lambda x: x.label): + output = generate_output_device(device, format_config) click.echo(f"\t({device.id}) - {device.label} - {device.type}") + click.echo(f"\t{output}") for channel in device.functionalChannels.values(): + fc_output = generate_output_functional_channel(channel, format_config) click.echo(f"\t\t[{channel.index}] - {channel.functionalChannelType:30} - {channel.label or ""}") + click.echo(f"\t\t\t{output}") @list.command() @@ -155,9 +163,12 @@ def groups(): """List all groups.""" runner = asyncio.run(get_initialized_runner()) model = runner.model + format_config = ConfigIO.get_output_format_config() print("Groups:") for group in sorted(model.groups.values(), key=lambda x: x.label): + output = generate_output_group(group, format_config) click.echo(f"\t({group.id}) - {group.label} - {group.type}") + click.echo(f"\t\t{output}") @list.command() @@ -317,7 +328,7 @@ def version(): @run.command() -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -342,7 +353,7 @@ def turn_on(id: str, channel: int = None): @run.command() -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -370,7 +381,7 @@ def turn_off(id: str, channel: int = None): @run.command() -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -417,7 +428,7 @@ def dump(filename, anonymized): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") def set_boost(id: str): """Set Boost on Group.""" runner = asyncio.run(get_initialized_runner()) @@ -431,7 +442,7 @@ def set_boost(id: str): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") def set_boost_stop(id: str): """Stop Boost on Group.""" runner = asyncio.run(get_initialized_runner()) @@ -447,7 +458,7 @@ def set_boost_stop(id: str): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-t", "--temperature", type=float, help="Target Temperature", required=True) def set_point_temperature(id: str, temperature: float): """Set point temperature for a group.""" @@ -469,7 +480,7 @@ def set_point_temperature(id: str, temperature: float): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-m", "--minutes", type=int, nargs=1, help="Duration of boost in Minutes", required=True) def set_boost_duration(id: str, minutes: int): """Sets the boost duration for a group in minutes""" @@ -486,7 +497,7 @@ def set_boost_duration(id: str, minutes: int): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-p", "--profile_index", type=str, required=True, help="index of the profile. Usually this is PROFILE_x. Use 'hmip list profiles ' to get a " "list of available profiles for a group.") @@ -506,7 +517,7 @@ def set_active_profile(id: str, profile_index: str): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("--mode", type=ClimateControlMode, help="the control mode. [ECO|AUTOMATIC|MANUAL]") def set_control_mode(id: str, mode: ClimateControlMode): """Set the control mode for a group.""" @@ -523,7 +534,7 @@ def set_control_mode(id: str, mode: ClimateControlMode): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -545,7 +556,7 @@ def set_display(id: str, channel: int, display: str): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-d", "--dim_level", type=click.FloatRange(0.0, 1.0), help="Target Dim Level", required=True) @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " @@ -576,7 +587,7 @@ def set_dim_level(id: str, dim_level: float, channel: int = None): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -604,7 +615,7 @@ def set_shutter_level(id: str, shutter_level: float, channel: int): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -631,7 +642,7 @@ def set_shutter_stop(id: str, shutter_level: float, channel: int): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -661,7 +672,7 @@ def set_slats_level(id: str, slats_level: float, shutter_level: float = None, ch @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-t", "--on_time", type=int, required=True, help="On time in seconds.") def set_on_time(id: str, on_time: int): """Set the on time for a group.""" @@ -678,7 +689,7 @@ def set_on_time(id: str, on_time: int): @run.command -@click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Only necessary, if you have more than one channel on the device. Not " "needed, if you want to control a group.") @@ -701,7 +712,7 @@ def toggle_garage_door(id: str, channel: int = None): @run.command -@click.option("--id", type=str, required=False, help="ID of the device or group, which the run command is applied to.") +@click.option("-id", type=str, required=False, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, help="Index of the Channel. Specify the channel index of a device, if you want to see available commands" "just for a single channel.") diff --git a/src/homematicip/cli/output_formatter/__init__.py b/src/homematicip/cli/output_formatter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/homematicip/cli/output_formatter/formatter.py b/src/homematicip/cli/output_formatter/formatter.py new file mode 100644 index 00000000..404aac76 --- /dev/null +++ b/src/homematicip/cli/output_formatter/formatter.py @@ -0,0 +1,26 @@ +from homematicip.configuration.output_format_config import OutputFormatConfig +from homematicip.model.model_components import FunctionalChannel + + +def generate_output_functional_channel(fc: FunctionalChannel, output_format_config: OutputFormatConfig) -> str: + """Generate a string representation of a FunctionalChannel object based on the output format configuration.""" + return _generate_output(fc, output_format_config.functional_channel_attributes) + + +def generate_output_group(group, output_format_config: OutputFormatConfig) -> str: + """Generate a string representation of a Group object based on the output format configuration.""" + return _generate_output(group, output_format_config.group_attributes) + + +def generate_output_device(device, output_format_config: OutputFormatConfig) -> str: + """Generate a string representation of a Device object based on the output format configuration.""" + return _generate_output(device, output_format_config.device_attributes) + +def _generate_output(group, attributes): + outputs = [] + + for attrib_name in attributes: + if hasattr(group, attrib_name): + outputs.append(f"{attrib_name}: {getattr(group, attrib_name)}") + + return ", ".join(outputs) \ No newline at end of file diff --git a/src/homematicip/configuration/config.py b/src/homematicip/configuration/config.py index 9fc67c28..9f9c6416 100644 --- a/src/homematicip/configuration/config.py +++ b/src/homematicip/configuration/config.py @@ -1,5 +1,6 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import logging +from typing import List @dataclass @@ -20,4 +21,4 @@ def from_persistent_config(cls, persistent_config: PersistentConfig): return cls(level=persistent_config.level, accesspoint_id=persistent_config.accesspoint_id, auth_token=persistent_config.auth_token, - log_file=persistent_config.log_file) \ No newline at end of file + log_file=persistent_config.log_file) diff --git a/src/homematicip/configuration/config_io.py b/src/homematicip/configuration/config_io.py index c9f4137b..03c059b5 100644 --- a/src/homematicip/configuration/config_io.py +++ b/src/homematicip/configuration/config_io.py @@ -4,6 +4,7 @@ from homematicip.configuration.config import PersistentConfig from homematicip.configuration.config_folder import get_well_known_folders, get_default_app_config_folder +from homematicip.configuration.output_format_config import OutputFormatConfig class ConfigIO: @@ -18,6 +19,19 @@ def find_config_in_well_known_locations(cls) -> PersistentConfig | None: return None + @classmethod + def get_output_format_config(cls) -> OutputFormatConfig | None: + """Find the configuration file in the well known locations. + @return the configuration if found, None otherwise.""" + config_file_path = os.path.join(get_default_app_config_folder(), "output-format.json") + if not os.path.exists(config_file_path): + config_file_path = os.path.join(os.getcwd(), "data/output-format.json") + + if not os.path.exists(config_file_path): + return None + + return cls.output_format_from_file(config_file_path) + @classmethod def _get_well_known_locations(cls) -> list[str]: """Return a list of well known locations where the configuration file can be found.""" @@ -27,18 +41,18 @@ def _get_well_known_locations(cls) -> list[str]: @classmethod def from_file(cls, file_path) -> PersistentConfig: """Open a file and load the configuration from it.""" - with open(file_path, "r", encoding='utf-8') as f: + with open(file_path, "r", encoding='utf-8') as f: json_config = json.load(f) config = PersistentConfig(**json_config) - # config_parser = ConfigParser() - # config_parser.read(file_path) - # - # config = PersistentConfig() - # config.auth_token = config_parser.get('AUTH', 'authtoken', fallback=None) - # config.accesspoint_id = config_parser.get('AUTH', 'accesspoint', fallback=None) - # config.level = int(config_parser.get('LOGGING', 'level', fallback=logging.INFO)) - # config.log_file = config_parser.get('LOGGING', 'log_file', fallback=None) + return config + + @classmethod + def output_format_from_file(cls, file_path) -> OutputFormatConfig: + """Open a file and load the output format configuration from it.""" + with open(file_path, "r", encoding='utf-8') as f: + json_config = json.load(f) + config = OutputFormatConfig(**json_config) return config @@ -46,16 +60,6 @@ def from_file(cls, file_path) -> PersistentConfig: def to_file(cls, config: PersistentConfig) -> str: """Write the configuration to a file. @return the file path of the written file.""" - # config_parser = ConfigParser() - # config_parser['AUTH'] = { - # 'authtoken': config.auth_token, - # 'accesspoint': config.accesspoint_id - # } - # config_parser['LOGGING'] = { - # 'level': config.level, - # 'log_file': config.log_file - # } - # filename = os.path.join(get_default_app_config_folder(), "config.json") with open(filename, 'w', encoding='utf-8') as file: diff --git a/src/homematicip/configuration/output_format_config.py b/src/homematicip/configuration/output_format_config.py new file mode 100644 index 00000000..5b5d44fd --- /dev/null +++ b/src/homematicip/configuration/output_format_config.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass, field +from typing import List + + +@dataclass +class OutputFormatConfig: + + functional_channel_attributes: List[str] = field(default_factory=list) + group_attributes: List[str] = field(default_factory=list) + device_attributes: List[str] = field(default_factory=list) \ No newline at end of file