Esempio n. 1
0
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        try:
            client.subscribe('hass/status')
            client.publish(
                f'sys-qtt/sensor/{SensorObject.device_name}/availability',
                'online',
                retain=True)
            c_print(f'{clr.B_OK}Success!', tab=1, status='ok')
            c_print(
                f'Updated {clr.B_HLT}{SensorObject.device_name}{clr.RESET} client on broker with '
                f'{clr.B_HLT}online{clr.RESET} status.',
                tab=1,
                status='info')
            global connected
            connected = True
        except Exception as e:
            c_print(
                f'Unable to publish {clr.B_HLT}online{clr.RESET} status to broker: '
                f'{clr.B_FAIL}{e}',
                tab=1,
                status='fail')
    elif rc == 5:
        c_print('Authentication failed.', tab=1, status='fail')
        raise ProgramKilled
    else:
        c_print('Failed to connect.', tab=1, status='fail')
Esempio n. 2
0
 def build_statics(self, sensor_dict: dict) -> dict:
     """Build the static sensor values if they're in the supplied dictionary"""
     failed_sensors = []
     for s in sensor_dict:
         properties = sensor_dict[s].properties
         if properties['static'] == True:
             try:
                 if properties['name'] in SensorValues.sensor_functions:
                     SensorValues.static_sensors[properties[
                         'name']] = SensorValues.sensor_functions[
                             properties['name']]()
                 else:
                     c_print(
                         f'Could not find function for {clr.B_HLT}{properties["name"]}{clr.RESET} '
                         f'static sensor. Removing from this session. Please remove from config.',
                         tab=2,
                         status='warning')
                     failed_sensors.append(s)
             except Exception as e:
                 c_print(
                     f'Unable to build {clr.B_HLT}{properties["name"]}{clr.RESET} static value, '
                     f'removed from list: {clr.B_FAIL}{e}',
                     tab=1,
                     status='fail')
                 failed_sensors.append(s)
     return failed_sensors
Esempio n. 3
0
def publish_sensor_configs(mqttClient):
    c_print('Publishing sensor configurations...', tab=1, status='wait')
    payload_size = 0
    for s in SENSOR_DICT:
        try:
            mqttClient.publish(topic=SENSOR_DICT[s].config.topic,
                               payload=SENSOR_DICT[s].config.payload,
                               qos=SENSOR_DICT[s].config.qos,
                               retain=SENSOR_DICT[s].config.retain)
            payload_size += 1
            #print(f'{SENSOR_DICT[s].config.payload}')
        except Exception as e:
            c_print(
                f'Could not publish {clr.B_HLT}{SENSOR_DICT[s].properties["name"]}{clr.RESET} sensor configuration: '
                f'{clr.B_FAIL}{e}',
                tab=2,
                status='warning')
    mqttClient.publish(
        f'sys-qtt/sensor/{SensorObject.device_name}/availability',
        'online',
        retain=True)
    c_print(
        f'{clr.B_HLT}{payload_size}{clr.RESET} sensor config{"s" if payload_size != 1 else ""} '
        f'and {clr.B_HLT}online{clr.RESET} status to broker.',
        tab=2,
        status='ok')
Esempio n. 4
0
 def value(self, sensor):
     try:
         # Mounted disk sensor functions are always called
         if 'path' in sensor.properties:
             return psutil.disk_usage(sensor.properties['path']).percent
         # Static sensors return values from outputs baked when Sys-QTT is first initialised
         elif sensor.properties['name'] in SensorValues.static_sensors:
             return SensorValues.static_sensors[sensor.properties['name']]
         # And dynamic sensors call their respective lambda functions
         elif sensor.properties['name'] in SensorValues.sensor_functions:
             return SensorValues.sensor_functions[
                 sensor.properties['name']]()
         else:
             c_print(
                 f'Unable to find {clr.B_HLT}{sensor.properties["name"]}'
                 f'{clr.RESET} in the session sensor objects.',
                 tab=2,
                 status='fail')
             return None
     # None returns
     except (TypeError, AttributeError):
         c_print(
             f'{clr.B_HLT}{sensor.properties["name"]}{clr.RESET} function returned '
             f'{clr.B_HLT}None{clr.RESET}.',
             tab=2,
             status='fail')
         return None
     # Missing functions in lambda expression
     except NameError as e:
         c_print(
             f'{clr.B_HLT}{sensor.properties["name"]}{clr.RESET} sensor '
             f'function is missing: {clr.B_FAIL}{e}',
             tab=2,
             status='fail')
         return None
     # General exception
     except Exception as e:
         c_print(
             f'Error while getting {clr.B_HLT}{sensor.properties["name"]}{clr.RESET} '
             f'value: {clr.B_FAIL}{e}',
             tab=2,
             status='fail')
         return None
Esempio n. 5
0
def create_scheduled_job():
    try:
        # Start the update job
        c_print(
            f'Adding {clr.B_HLT}sensor update{clr.RESET} job on '
            f'{clr.B_HLT}{CONFIG["general"]["update_interval"]}{clr.RESET} second schedule...',
            status='wait')
        job = schedule.every(CONFIG["general"]["update_interval"]).seconds.do(
            publish_sensor_values)
        c_print(f'{clr.B_HLT}{schedule.get_jobs()}', tab=1, status='ok')
        return job
    except Exception as e:
        c_print(f'Unable to add job: {clr.B_FAIL}{e}', tab=1, status='fail')
        sys.exit()
Esempio n. 6
0
def on_disconnect(client, userdata, rc):
    global connected
    connected = False
    print()
    c_print(f'{clr.B_FAIL}Disconnected!', tab=1, status='fail')
    if rc != 0:
        c_print(
            'Unexpected MQTT disconnection. Will attempt to re-establish connection.',
            tab=2,
            status='fail')
    else:
        c_print(f'RC value: {clr.B_HLT}{rc}', tab=2, status='info')
    if not program_killed:
        print()
        connect_to_broker()
Esempio n. 7
0
def initialise_config(config_dict) -> dict:
    c_print('Processing config...', status='wait')
    _required_general = [
        'broker_host', 'broker_user', 'broker_pass', 'device_name',
        'client_id', 'timezone'
    ]
    _default_config = {
        'broker_port': 1883,
        'update_interval': 60,
        'retry_time': 10,
        'allowed_sensor_fails': 0
    }

    # Check for missing required configs
    if 'general' not in config_dict:
        c_print(
            f'{clr.B_HLT}"general"{clr.RESET} category not defined in config file. '
            f'Was it deleted by accident? Please recreate config.yaml using /examples/config.yaml.',
            tab=1,
            status='fail')
        raise ProgramKilled
    if 'sensors' not in config_dict:
        c_print(
            f'{clr.B_HLT}"sensors"{clr.RESET} category not defined in config file. '
            f'Was it deleted by accident? Please recreate config.yaml using /examples/config.yaml.',
            tab=1,
            status='fail')
        raise ProgramKilled
    if len(missing :=
           [x for x in _required_general
            if x not in config_dict['general']]) > 0:
        for m in missing:
            c_print(
                f'{clr.B_HLT}{m}{clr.RESET} not defined in config file and is required. '
                f'Please check the documentation.',
                tab=1,
                status='fail')
        raise ProgramKilled
Esempio n. 8
0
def import_config_yaml():
    """Import config.yaml either via supplied argument, or check in default location"""
    c_print('Importing config.yaml...', status='wait')
    try:
        args = _parser().parse_args()
        config_file = args.config
        with open(config_file) as f:
            config_yaml = yaml.safe_load(f)
        c_print(f'Config file found: {CONFIG_PATH}', tab=1, status='ok')
        return config_yaml
    except Exception as e:
        c_print(
            f'{clr.B_HLT}Could not find config.yaml file. Please check the documentation: {e}',
            status='fail')
        print()
        sys.exit()
Esempio n. 9
0
def publish_sensor_values():
    if not connected or program_killed:
        return None
    c_print('Sending update sensor payload...', status='wait')
    payload_size = 0
    failed_size = 0
    # Payload construction
    payload_str = f'{{'
    for s in SENSOR_DICT:
        try:
            payload_str += f'"{s}": "{VALUE_GENERATOR.value(SENSOR_DICT[s])}",'
            payload_size += 1
        except Exception as e:
            c_print(
                f'Error while adding {clr.B_HLT}{s}{clr.RESET} '
                f'to update payload: {clr.B_FAIL}{e}',
                tab=1,
                status='fail')
            failed_size += 1
    payload_str = payload_str[:-1]
    payload_str += f'}}'

    # Report failed sensors
    if failed_size > 0:
        c_print(
            f'{clr.B_HLT}{failed_size}{clr.RESET} sensor '
            f'update{"s" if failed_size > 1 else ""} unable to be sent.',
            tab=1,
            status='fail')

    # Now let's ship this sucker off!
    try:
        MQTT_CLIENT.publish(
            topic=f'sys-qtt/sensor/{SensorObject.device_name}/state',
            payload=payload_str,
            qos=1,
            retain=False)
    except Exception as e:
        c_print(f'Unable to publish update payload: {clr.B_FAIL}{e}',
                tab=1,
                status='fail')

    c_print(
        f'{clr.B_HLT}{payload_size}{clr.RESET} sensor '
        f'update{"s" if payload_size > 1 else ""} sent to MQTT broker.',
        tab=1,
        status='ok')
    c_print(
        f'{clr.B_HLT}{CONFIG["general"]["update_interval"]}{clr.RESET} '
        f'seconds until next update...',
        tab=1,
        status='wait')
Esempio n. 10
0
def on_message(client, userdata, message):
    c_print(
        f'Message received from broker: {clr.B_HLT}{message.payload.decode()}',
        status='info')
    if (message.payload.decode() == 'online'):
        publish_sensor_configs(client)
Esempio n. 11
0
def connect_to_broker():
    """Initiates connection with MQTT server """
    while True:
        try:
            c_print(
                f'Attempting to reach MQTT broker at {clr.B_HLT}{CONFIG["general"]["broker_host"]}{clr.RESET} on port '
                f'{clr.B_HLT}{CONFIG["general"]["broker_port"]}{clr.RESET}...',
                status='wait')
            MQTT_CLIENT.connect(CONFIG['general']['broker_host'],
                                CONFIG['general']['broker_port'])
            c_print(f'{clr.B_OK}MQTT broker responded.', tab=1, status='ok')
            break
        except ConnectionRefusedError as e:
            c_print(f'MQTT broker is down or unreachable: {clr.B_FAIL}{e}',
                    tab=1,
                    status='fail')
        except OSError as e:
            c_print(f'Network I/O error. Is the network down? {clr.B_FAIL}{e}',
                    tab=1,
                    status='fail')
        except Exception as e:
            c_print(f'Terminating connection attempt: {clr.B_FAIL}{e}',
                    tab=1,
                    status='fail')
        c_print(
            f'Trying again in {clr.B_HLT}{CONFIG["general"]["retry_time"]}{clr.RESET} seconds...',
            tab=1,
            status='wait')
        time.sleep(CONFIG["general"]["retry_time"])
    try:
        publish_sensor_configs(MQTT_CLIENT)
    except Exception as e:
        c_print(f'Unable to publish sensor config: {clr.B_FAIL}{e}',
                tab=1,
                status='fail')
        raise ProgramKilled
Esempio n. 12
0
def import_sensors(sensor_dict: dict) -> dict:
    # Main sensor config import
    c_print('Importing sensor configurations...', status='wait')
    for sensor in CONFIG['sensors']:
        if sensor not in PROPERTIES:
            c_print(
                f'{clr.B_HLT}{sensor}{clr.RESET} missing from {clr.B_HLT}{PROPERTIES_FILE}{clr.RESET}. Skipping.',
                tab=1,
                status='warning')
            continue
        # Skip if unknown value provided
        if CONFIG['sensors'][sensor] not in [
                False, 'off', True, 'on', 'dynamic', 'static'
        ]:
            c_print(
                f'Unknown value {clr.B_HLT}{CONFIG["sensors"][sensor]}{clr.RESET} for {clr.B_HLT}{sensor}'
                f'{clr.RESET}. Allowed values: {clr.B_HLT}'
                f'"off", "on", "dynamic", "static"{clr.RESET}. Please check {clr.B_HLT}config.yaml'
                f'{clr.RESET}.',
                tab=1,
                status='warning')
            continue
        # Ignore sensors turned off
        if CONFIG['sensors'][sensor] in ['off', False]:
            continue
        # Fitler duplicate entries
        if sensor in sensor_dict:
            c_print(
                f'Multiple {clr.B_HLT}{sensor}{clr.RESET} in {clr.B_HLT}config.yaml{clr.RESET}.'
                f'Ignoring duplicate. Remove from config to silence this warning.',
                tab=1,
                status='warning')
            continue
        # Add valid sensor to use in this session
        try:
            PROPERTIES[sensor]['name'] = sensor
            PROPERTIES[sensor]['static'] = CONFIG['sensors'][
                sensor] == 'static'
            sensor_dict[sensor] = SensorObject(PROPERTIES[sensor])
        except Exception as e:
            c_print(
                f'Unable add {clr.B_HLT}{sensor}{clr.RESET}and has been removed from session: {clr.B_FAIL}{e}',
                tab=1,
                status='fail')

    # Mounted disk sensor config import
    _mnt = 'disk_mounted'
    if _mnt in CONFIG and CONFIG[_mnt] is not None:
        for d in CONFIG[_mnt]:
            # Skip duplicate names
            if d in sensor_dict:
                c_print(
                    f'Mounted disk {clr.B_HLT}{d}{clr.RESET} has the same name as another sensor. '
                    f'Remove from config or change its name to stop this message.',
                    tab=1,
                    status='warning')
                continue
            # Skip mounted disk sensor if no path provided
            if CONFIG[_mnt][d] is None:
                c_print(
                    f'{clr.B_HLT}{d}{clr.RESET} mounted disk config entry is{clr.B_HLT}'
                    f' missing a volume path{clr.RESET}. Skipping. Check config.yaml.',
                    tab=1,
                    status='warning')
                continue
            # Skip mounted drive paths that do not resolve a valid directory
            if not path.isdir(CONFIG[_mnt][d]):
                c_print(
                    f'{clr.B_HLT}{d}{clr.RESET} mounted disk path {clr.B_HLT}{CONFIG[_mnt][d]}'
                    f'{clr.RESET} is not a valid directory. Skipping. Check config.yaml.',
                    tab=1,
                    status='warning')
                continue
            # Add valid mounted disk sensor to use in this session
            try:
                drive_properties = PROPERTIES[_mnt]
                drive_properties['name'] = f'disk_{d.replace(" ","_").lower()}'
                drive_properties['title'] = f'Disk {d} Use'
                drive_properties['path'] = CONFIG[_mnt][d]
                drive_properties['static'] = False
                # Name mounted disk sensor internally with "disk_" prefix name
                sensor_dict[drive_properties['name']] = SensorObject(
                    drive_properties)
            except Exception as e:
                c_print(
                    f'Unable add {clr.B_HLT}{d}{clr.RESET} mounted disk and has been removed '
                    f'from this session: {clr.B_FAIL}{e}',
                    tab=1,
                    status='fail')

    c_print(
        f'Imported {clr.B_HLT}{len(sensor_dict)}{clr.RESET} sensor properties.',
        tab=1,
        status='ok')

    # Initialise static sensors
    c_print(f'Initialising {clr.B_HLT}static{clr.RESET} sensors...',
            tab=1,
            status='wait')
    failed_sensors = VALUE_GENERATOR.build_statics(sensor_dict)
    if len(failed_sensors) > 0:
        for f in failed_sensors:
            sensor_dict.pop(f)
        c_print(
            f'{clr.B_HLT}{len(failed_sensors)}{clr.RESET} static sensors have been removed from this session. '
            f'Please check your config!',
            tab=2,
            status='warning')
    c_print(f'Static sensors built.', tab=2, status='ok')
    # Perform sensor value check on all sensor objects and remove ones that fail to generate a value
    c_print(f'Checking output of each sensor...', tab=1, status='wait')
    failed_sensors = {}
    for sensor in sensor_dict:
        if (value := VALUE_GENERATOR.value(sensor_dict[sensor])) is not None:
            c_print(
                f'{clr.B_HLT}{sensor}{clr.RESET} returned: {clr.B_HLT}{value} '
                + (f'{sensor_dict[sensor].properties["unit"]}'
                   if 'unit' in sensor_dict[sensor].properties else ''),
                tab=2,
                status='ok')
        else:
            failed_sensors[sensor] = 'off'
Esempio n. 13
0
           [x for x in _required_general
            if x not in config_dict['general']]) > 0:
        for m in missing:
            c_print(
                f'{clr.B_HLT}{m}{clr.RESET} not defined in config file and is required. '
                f'Please check the documentation.',
                tab=1,
                status='fail')
        raise ProgramKilled

    # Apply default configs if required
    for d in _default_config:
        if d not in config_dict['general']:
            c_print(
                f'{clr.B_HLT}{d}{clr.RESET} not defined in config file. '
                f'Defaulting to {clr.B_HLT}{_default_config[d]}{clr.RESET}.',
                tab=1,
                status='ok')
            config_dict['general'][d] = _default_config[d]
    c_print(f'Config initialised.', tab=1, status='ok')

    # Apply timezone
    set_timezone(config_dict['general']['timezone'])

    # Import sensor properties
    global PROPERTIES
    c_print('Importing sensor properties...', status='wait', tab=1)
    try:
        with open(PROPERTIES_PATH, 'r') as infile:
            PROPERTIES = json.load(infile)
    except Exception as e: