diff --git a/picframe/config/configuration_example.yaml b/picframe/config/configuration_example.yaml index f16f25a..6f05253 100644 --- a/picframe/config/configuration_example.yaml +++ b/picframe/config/configuration_example.yaml @@ -14,6 +14,7 @@ viewer: show_text: "title caption name date folder location" # default="title caption name date folder location", show text, include combination of words: title, caption name, date, location, folder text_justify: "L" # text justification L, C or R text_bkg_hgt: 0.25 # default=0.25 (0.0-1.0), percentage of screen height for text background texture + text_opacity: 1.0 # default=1.0 (0.0-1.0), alpha value of text overlay fit: False # default=False, True => scale image so all visible and leave 'gaps' # False => crop image so no 'gaps' kenburns: False # default=False, will set fit->False and blur_edges->False @@ -38,9 +39,11 @@ viewer: clock_justify: "R" # default="R", clock justification L, C, or R clock_text_sz: 120 # default=120, clock character size clock_format: "%I:%M" # default="%I:%M", strftime format for clock string + clock_opacity: 1.0 # default=1.0 (0.0-1.0), alpha value of clock overlay menu_text_sz: 40 # default=40, menu character size menu_autohide_tm: 10.0 # default=10.0, time in seconds to show menu before auto hiding (0 disables auto hiding) + geo_suppress_list: [] # default=None, substrings to remove from the location text model: pic_dir: "~/Pictures" # default="~/Pictures", root folder for images diff --git a/picframe/controller.py b/picframe/controller.py index 79aa79d..9ea6a6a 100644 --- a/picframe/controller.py +++ b/picframe/controller.py @@ -69,6 +69,7 @@ def paused(self, val:bool): self.__paused = val pic = self.__model.get_current_pics()[0] # only refresh left text self.__viewer.reset_name_tm(pic, val, side=0, pair=self.__model.get_current_pics()[1] is not None) + self.publish_state() def next(self): self.__next_tm = 0 @@ -83,7 +84,7 @@ def back(self): def delete(self): self.__model.delete_file() - self.back() # TODO check needed to avoid skipping one as record has been deleted from model.__file_list + self.next() # TODO check needed to avoid skipping one as record has been deleted from model.__file_list self.__next_tm = 0 def set_show_text(self, txt_key=None, val="ON"): @@ -154,6 +155,7 @@ def display_is_on(self): def display_is_on(self, on_off): self.paused = not on_off self.__viewer.display_is_on = on_off + self.publish_state() @property def clock_is_on(self): @@ -172,6 +174,7 @@ def shuffle(self, val:bool): self.__model.shuffle = val self.__model.force_reload() self.__next_tm = 0 + self.publish_state() @property def fade_time(self): @@ -202,7 +205,7 @@ def brightness(self): @brightness.setter def brightness(self, val): self.__viewer.set_brightness(float(val)) - self.__next_tm = 0 + self.publish_state() @property def matting_images(self): diff --git a/picframe/interface_mqtt.py b/picframe/interface_mqtt.py index 10886ab..cd534fa 100644 --- a/picframe/interface_mqtt.py +++ b/picframe/interface_mqtt.py @@ -46,21 +46,23 @@ def __init__(self, controller, mqtt_config): self.__device_id = mqtt_config['device_id'] self.__device_url = mqtt_config['device_url'] except Exception as e: - self.__logger.info("MQTT not set up because of: {}".format(e)) + self.__logger.error("MQTT not set up because of: {}".format(e)) + raise def start(self): try: self.__controller.publish_state = self.publish_state self.__client.loop_start() except Exception as e: - self.__logger.info("MQTT not started because of: {}".format(e)) + self.__logger.error("MQTT not started because of: {}".format(e)) + raise def stop(self): try: self.__controller.publish_state = None self.__client.loop_stop() except Exception as e: - self.__logger.info("MQTT stopping failed because of: {}".format(e)) + self.__logger.error("MQTT stopping failed because of: {}".format(e)) def on_connect(self, client, userdata, flags, rc): @@ -69,78 +71,78 @@ def on_connect(self, client, userdata, flags, rc): return self.__logger.info('Connected with mqtt broker') - sensor_topic_head = "homeassistant/sensor/" + self.__device_id - number_topic_head = "homeassistant/number/" + self.__device_id - select_topic_head = "homeassistant/select/" + self.__device_id - switch_topic_head = "homeassistant/switch/" + self.__device_id - # send last will and testament - available_topic = switch_topic_head + "/available" + available_topic = "homeassistant/switch/" + self.__device_id + "/available" client.publish(available_topic, "online", qos=0, retain=True) ## sensors - self.__setup_sensor(client, sensor_topic_head, "date_from", "mdi:calendar-arrow-left", available_topic, entity_category="config") - self.__setup_sensor(client, sensor_topic_head, "date_to", "mdi:calendar-arrow-right", available_topic, entity_category="config") - self.__setup_sensor(client, sensor_topic_head, "location_filter", "mdi:map-search", available_topic, entity_category="config") - self.__setup_sensor(client, sensor_topic_head, "tags_filter", "mdi:image-search", available_topic, entity_category="config") - self.__setup_sensor(client, sensor_topic_head, "image_counter", "mdi:camera-burst", available_topic, entity_category="diagnostic") - self.__setup_sensor(client, sensor_topic_head, "image", "mdi:file-image", available_topic, has_attributes=True, entity_category="diagnostic") + self.__setup_sensor(client, "date_from", "mdi:calendar-arrow-left", available_topic, entity_category="config") + self.__setup_sensor(client, "date_to", "mdi:calendar-arrow-right", available_topic, entity_category="config") + self.__setup_sensor(client, "location_filter", "mdi:map-search", available_topic, entity_category="config") + self.__setup_sensor(client, "tags_filter", "mdi:image-search", available_topic, entity_category="config") + self.__setup_sensor(client, "image_counter", "mdi:camera-burst", available_topic, entity_category="diagnostic") + self.__setup_sensor(client, "image", "mdi:file-image", available_topic, has_attributes=True, entity_category="diagnostic") ## numbers - self.__setup_number(client, number_topic_head, "brightness", 0.0, 1.0, 0.1, "mdi:brightness-6", available_topic) - self.__setup_number(client, number_topic_head, "time_delay", 1, 400, 1, "mdi:image-plus", available_topic) - self.__setup_number(client, number_topic_head, "fade_time", 1, 50, 1,"mdi:image-size-select-large", available_topic) - self.__setup_number(client, number_topic_head, "matting_images", 0.0, 1.0, 0.01, "mdi:image-frame", available_topic) + self.__setup_number(client, "brightness", 0.0, 1.0, 0.1, "mdi:brightness-6", available_topic) + self.__setup_number(client, "time_delay", 1, 400, 1, "mdi:image-plus", available_topic) + self.__setup_number(client, "fade_time", 1, 50, 1,"mdi:image-size-select-large", available_topic) + self.__setup_number(client, "matting_images", 0.0, 1.0, 0.01, "mdi:image-frame", available_topic) ## selects _, dir_list = self.__controller.get_directory_list() dir_list.sort() - self.__setup_select(client, select_topic_head, "directory", dir_list, "mdi:folder-multiple-image", available_topic, init=True) + self.__setup_select(client, "directory", dir_list, "mdi:folder-multiple-image", available_topic, init=True) command_topic = self.__device_id + "/directory" client.subscribe(command_topic, qos=0) ## switches - self.__setup_switch(client, switch_topic_head, "_text_refresh", "mdi:refresh", available_topic, entity_category="config") - self.__setup_switch(client, switch_topic_head, "_delete", "mdi:delete", available_topic) - self.__setup_switch(client, switch_topic_head, "_name_toggle", "mdi:subtitles", available_topic, + self.__setup_switch(client, "_text_refresh", "mdi:refresh", available_topic, entity_category="config") + self.__setup_switch(client, "_name_toggle", "mdi:subtitles", available_topic, self.__controller.text_is_on("name"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_title_toggle", "mdi:subtitles", available_topic, + self.__setup_switch(client, "_title_toggle", "mdi:subtitles", available_topic, self.__controller.text_is_on("title"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_caption_toggle", "mdi:subtitles", available_topic, + self.__setup_switch(client, "_caption_toggle", "mdi:subtitles", available_topic, self.__controller.text_is_on("caption"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_date_toggle", "mdi:calendar-today", available_topic, + self.__setup_switch(client, "_date_toggle", "mdi:calendar-today", available_topic, self.__controller.text_is_on("date"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_location_toggle", "mdi:crosshairs-gps", available_topic, + self.__setup_switch(client, "_location_toggle", "mdi:crosshairs-gps", available_topic, self.__controller.text_is_on("location"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_directory_toggle", "mdi:folder", available_topic, + self.__setup_switch(client, "_directory_toggle", "mdi:folder", available_topic, self.__controller.text_is_on("directory"), entity_category="config") - self.__setup_switch(client, switch_topic_head, "_text_off", "mdi:badge-account-horizontal-outline", available_topic, entity_category="config") - self.__setup_switch(client, switch_topic_head, "_display", "mdi:panorama", available_topic, + self.__setup_switch(client, "_text_off", "mdi:badge-account-horizontal-outline", available_topic, entity_category="config") + self.__setup_switch(client, "_display", "mdi:panorama", available_topic, self.__controller.display_is_on) - self.__setup_switch(client, switch_topic_head, "_clock", "mdi:clock-outline", available_topic, + self.__setup_switch(client, "_clock", "mdi:clock-outline", available_topic, self.__controller.clock_is_on, entity_category="config") - self.__setup_switch(client, switch_topic_head, "_shuffle", "mdi:shuffle-variant", available_topic, + self.__setup_switch(client, "_shuffle", "mdi:shuffle-variant", available_topic, self.__controller.shuffle) - self.__setup_switch(client, switch_topic_head, "_paused", "mdi:pause", available_topic, + self.__setup_switch(client, "_paused", "mdi:pause", available_topic, self.__controller.paused) - self.__setup_switch(client, switch_topic_head, "_back", "mdi:skip-previous", available_topic) - self.__setup_switch(client, switch_topic_head, "_next", "mdi:skip-next", available_topic) + + # buttons + self.__setup_button(client, "_delete", "mdi:delete", available_topic) + self.__setup_button(client, "_back", "mdi:skip-previous", available_topic) + self.__setup_button(client, "_next", "mdi:skip-next", available_topic) client.subscribe(self.__device_id + "/purge_files", qos=0) # close down without killing! client.subscribe(self.__device_id + "/stop", qos=0) # close down without killing! - def __setup_sensor(self, client, sensor_topic_head, topic, icon, available_topic, has_attributes=False, entity_category=None): + def __setup_sensor(self, client, topic, icon, available_topic, has_attributes=False, entity_category=None): + sensor_topic_head = "homeassistant/sensor/" + self.__device_id config_topic = sensor_topic_head + "_" + topic + "/config" name = self.__device_id + "_" + topic dict = {"name": name, "icon": icon, - "state_topic": sensor_topic_head + "/state", "value_template": "{{ value_json." + topic + "}}", "avty_t": available_topic, "uniq_id": name, "dev":{"ids":[self.__device_id]}} if has_attributes == True: + dict["state_topic"] = sensor_topic_head + "_" + topic + "/state" dict["json_attributes_topic"] = sensor_topic_head + "_" + topic + "/attributes" + else: + dict["state_topic"] = sensor_topic_head + "/state" if entity_category: dict["entity_category"] = entity_category @@ -148,7 +150,8 @@ def __setup_sensor(self, client, sensor_topic_head, topic, icon, available_topic client.publish(config_topic, config_payload, qos=0, retain=True) client.subscribe(self.__device_id + "/" + topic, qos=0) - def __setup_number(self, client, number_topic_head, topic, min, max, step, icon, available_topic): + def __setup_number(self, client, topic, min, max, step, icon, available_topic): + number_topic_head = "homeassistant/number/" + self.__device_id config_topic = number_topic_head + "_" + topic + "/config" command_topic = self.__device_id + "/" + topic state_topic = "homeassistant/sensor/" + self.__device_id + "/state" @@ -168,7 +171,8 @@ def __setup_number(self, client, number_topic_head, topic, min, max, step, icon, client.publish(config_topic, config_payload, qos=0, retain=True) client.subscribe(command_topic, qos=0) - def __setup_select(self, client, select_topic_head, topic, options, icon, available_topic, init=False): + def __setup_select(self, client, topic, options, icon, available_topic, init=False): + select_topic_head = "homeassistant/select/" + self.__device_id config_topic = select_topic_head + "_" + topic + "/config" command_topic = self.__device_id + "/" + topic state_topic = "homeassistant/sensor/" + self.__device_id + "/state" @@ -188,8 +192,9 @@ def __setup_select(self, client, select_topic_head, topic, options, icon, availa if init: client.subscribe(command_topic, qos=0) - def __setup_switch(self, client, switch_topic_head, topic, icon, + def __setup_switch(self, client, topic, icon, available_topic, is_on=False, entity_category=None): + switch_topic_head = "homeassistant/switch/" + self.__device_id config_topic = switch_topic_head + topic + "/config" command_topic = switch_topic_head + topic + "/set" state_topic = switch_topic_head + topic + "/state" @@ -215,9 +220,36 @@ def __setup_switch(self, client, switch_topic_head, topic, icon, client.publish(config_topic, config_payload, qos=0, retain=True) client.publish(state_topic, "ON" if is_on else "OFF", qos=0, retain=True) + def __setup_button(self, client, topic, icon, + available_topic, entity_category=None): + button_topic_head = "homeassistant/button/" + self.__device_id + config_topic = button_topic_head + topic + "/config" + command_topic = button_topic_head + topic + "/set" + dict = {"name": self.__device_id + topic, + "icon": icon, + "command_topic": command_topic, + "payload_press": "ON", + "avty_t": available_topic, + "uniq_id": self.__device_id + topic, + "dev": { + "ids": [self.__device_id], + "name": self.__device_id, + "mdl": "PictureFrame", + "sw": __version__, + "mf": "pi3d PictureFrame project"}} + if self.__device_url : + dict["dev"]["cu"] = self.__device_url + if entity_category: + dict["entity_category"] = entity_category + config_payload = json.dumps(dict) + + client.subscribe(command_topic , qos=0) + client.publish(config_topic, config_payload, qos=0, retain=True) + def on_message(self, client, userdata, message): msg = message.payload.decode("utf-8") switch_topic_head = "homeassistant/switch/" + self.__device_id + button_topic_head = "homeassistant/button/" + self.__device_id ###### switches ###### # display @@ -257,22 +289,16 @@ def on_message(self, client, userdata, message): self.__controller.paused = False client.publish(state_topic, "OFF", retain=True) # back buttons - elif message.topic == switch_topic_head + "_back/set": - state_topic = switch_topic_head + "_back/state" + elif message.topic == button_topic_head + "_back/set": if msg == "ON": - client.publish(state_topic, "OFF", retain=True) self.__controller.back() # next buttons - elif message.topic == switch_topic_head + "_next/set": - state_topic = switch_topic_head + "_next/state" + elif message.topic == button_topic_head + "_next/set": if msg == "ON": - client.publish(state_topic, "OFF", retain=True) self.__controller.next() # delete - elif message.topic == switch_topic_head + "_delete/set": - state_topic = switch_topic_head + "_delete/state" + elif message.topic == button_topic_head + "_delete/set": if msg == "ON": - client.publish(state_topic, "OFF", retain=True) self.__controller.delete() # title on elif message.topic == switch_topic_head + "_title_toggle/set": @@ -381,13 +407,27 @@ def on_message(self, client, userdata, message): elif message.topic == self.__device_id + "/stop": self.__controller.stop() - def publish_state(self, image, image_attr): - sensor_topic_head = "homeassistant/sensor/" + self.__device_id + def publish_state(self, image=None, image_attr=None): + sensor_topic_head = "homeassistant/sensor/" + self.__device_id switch_topic_head = "homeassistant/switch/" + self.__device_id - select_topic_head = "homeassistant/select/" + self.__device_id - sensor_state_topic = sensor_topic_head + "/state" + available_topic = switch_topic_head + "/available" sensor_state_payload = {} + image_state_payload = {} + + ## image + # image attributes + if image_attr is not None: + attributes_topic = sensor_topic_head + "_image/attributes" + self.__logger.debug("Send image attributes: %s", image_attr) + self.__client.publish(attributes_topic, json.dumps(image_attr), qos=0, retain=False) + # image sensor + if image is not None: + _, tail = os.path.split(image) + image_state_payload["image"] = tail + image_state_topic = sensor_topic_head + "_image/state" + self.__logger.info("Send image state: %s", image_state_payload) + self.__client.publish(image_state_topic, json.dumps(image_state_payload), qos=0, retain=False) ## sensor # directory sensor @@ -395,9 +435,6 @@ def publish_state(self, image, image_attr): sensor_state_payload["directory"] = actual_dir # image counter sensor sensor_state_payload["image_counter"] = str(self.__controller.get_number_of_files()) - # image sensor - _, tail = os.path.split(image) - sensor_state_payload["image"] = tail # date_from sensor_state_payload["date_from"] = int(self.__controller.date_from) # date_to @@ -406,7 +443,6 @@ def publish_state(self, image, image_attr): sensor_state_payload["location_filter"] = self.__controller.location_filter # tags_filter sensor_state_payload["tags_filter"] = self.__controller.tags_filter - ## number state # time_delay sensor_state_payload["time_delay"] = self.__controller.time_delay @@ -417,17 +453,28 @@ def publish_state(self, image, image_attr): # matting_images sensor_state_payload["matting_images"] = self.__controller.matting_images - # send last will and testament - available_topic = switch_topic_head + "/available" - self.__client.publish(available_topic, "online", qos=0, retain=True) - #pulish sensors - attributes_topic = sensor_topic_head + "_image/attributes" - self.__logger.debug("Send image attributes: %s", image_attr) - self.__client.publish(attributes_topic, json.dumps(image_attr), qos=0, retain=False) dir_list.sort() - self.__setup_select(self.__client, select_topic_head, "directory", dir_list, "mdi:folder-multiple-image", available_topic, init=False) + self.__setup_select(self.__client, "directory", dir_list, "mdi:folder-multiple-image", available_topic, init=False) self.__logger.info("Send sensor state: %s", sensor_state_payload) + sensor_state_topic = sensor_topic_head + "/state" self.__client.publish(sensor_state_topic, json.dumps(sensor_state_payload), qos=0, retain=False) + # publish state of switches + # pause + state_topic = switch_topic_head + "_paused/state" + payload = "ON" if self.__controller.paused else "OFF" + self.__client.publish(state_topic, payload, retain=True) + # shuffle + state_topic = switch_topic_head + "_shuffle/state" + payload = "ON" if self.__controller.shuffle else "OFF" + self.__client.publish(state_topic, payload, retain=True) + # display + state_topic = switch_topic_head + "_display/state" + payload = "ON" if self.__controller.display_is_on else "OFF" + self.__client.publish(state_topic, payload, retain=True) + + # send last will and testament + self.__client.publish(available_topic, "online", qos=0, retain=True) + diff --git a/picframe/model.py b/picframe/model.py index 4987a46..eba82a3 100644 --- a/picframe/model.py +++ b/picframe/model.py @@ -26,6 +26,7 @@ 'show_text': "name location", 'text_justify': 'L', 'text_bkg_hgt': 0.25, + 'text_opacity': 1.0, 'fit': False, #'auto_resize': True, 'kenburns': False, @@ -48,9 +49,11 @@ 'clock_justify': "R", 'clock_text_sz': 120, 'clock_format': "%I:%M", + 'clock_opacity': 1.0, #'codepoints': "1234567890AÄÀÆÅÃBCÇDÈÉÊEËFGHIÏÍJKLMNÑOÓÖÔŌØPQRSTUÚÙÜVWXYZaáàãæåäbcçdeéèêëfghiíïjklmnñoóôōøöpqrsßtuúüvwxyz., _-+*()&/`´'•" # limit to 121 ie 11x11 grid_size 'menu_text_sz': 40, 'menu_autohide_tm': 10.0, + 'geo_suppress_list': [], }, 'model': { diff --git a/picframe/start.py b/picframe/start.py index 8926e48..fe7ab87 100644 --- a/picframe/start.py +++ b/picframe/start.py @@ -138,8 +138,14 @@ def main(): mqtt_config = m.get_mqtt_config() if mqtt_config['use_mqtt']: from picframe import interface_mqtt - mqtt = interface_mqtt.InterfaceMQTT(c, mqtt_config) - mqtt.start() + try: + mqtt = interface_mqtt.InterfaceMQTT(c, mqtt_config) + mqtt.start() + except: + logger.error("Can't initialize mqtt. Stopping picframe") + sys.exit(1) + + http_config = m.get_http_config() model_config = m.get_model_config() diff --git a/picframe/viewer_display.py b/picframe/viewer_display.py index 3dd3be7..f8cd86e 100644 --- a/picframe/viewer_display.py +++ b/picframe/viewer_display.py @@ -59,7 +59,9 @@ def __init__(self, config): self.__show_text = parse_show_text(config['show_text']) self.__text_justify = config['text_justify'].upper() self.__text_bkg_hgt = config['text_bkg_hgt'] if 0 <= config['text_bkg_hgt'] <= 1 else 0.25 + self.__text_opacity = config['text_opacity'] self.__fit = config['fit'] + self.__geo_suppress_list = config['geo_suppress_list'] #self.__auto_resize = config['auto_resize'] self.__kenburns = config['kenburns'] if self.__kenburns: @@ -96,6 +98,7 @@ def __init__(self, config): self.__clock_justify = config['clock_justify'] self.__clock_text_sz = config['clock_text_sz'] self.__clock_format = config['clock_format'] + self.__clock_opacity = config['clock_opacity'] ImageFile.LOAD_TRUNCATED_IMAGES = True # occasional damaged file hangs app @property @@ -358,7 +361,16 @@ def __make_text(self, pic, paused, side=0, pair=False): fdt = time.strftime(self.__show_text_fm, time.localtime(pic.exif_datetime)) info_strings.append(fdt) if (self.__show_text & 16) == 16 and pic.location is not None: # location - info_strings.append(pic.location) #TODO need to sanitize and check longer than 0 for real + location = pic.location + # search for and remove substrings from the location text + if self.__geo_suppress_list is not None: + for part in self.__geo_suppress_list: + location = location.replace(part, "") + # remove any redundant concatination strings once the substrings have been removed + location = location.replace(" ,", "") + # remove any trailing commas or spaces from the location + location = location.strip(", ") + info_strings.append(location) #TODO need to sanitize and check longer than 0 for real if (self.__show_text & 32) == 32: # folder info_strings.append(os.path.basename(os.path.dirname(pic.fname))) if paused: @@ -372,7 +384,8 @@ def __make_text(self, pic, paused, side=0, pair=False): else: c_rng = self.__display.width * 0.5 - 100 # range for x loc from L to R justified block = pi3d.FixedString(self.__font_file, final_string, shadow_radius=3, font_size=self.__show_text_sz, - shader=self.__flat_shader, justify=self.__text_justify, width=c_rng) + shader=self.__flat_shader, justify=self.__text_justify, width=c_rng, + color=(255, 255, 255, int(255 * float(self.__text_opacity)))) adj_x = (c_rng - block.sprite.width) // 2 # half amount of space outside sprite if self.__text_justify == "L": adj_x *= -1 @@ -398,7 +411,8 @@ def __draw_clock(self): if current_time != self.__prev_clock_time: width = self.__display.width - 50 self.__clock_overlay = pi3d.FixedString(self.__font_file, current_time, font_size=self.__clock_text_sz, - shader=self.__flat_shader, width=width, shadow_radius=3) + shader=self.__flat_shader, width=width, shadow_radius=3, + color=(255, 255, 255, int(255 * float(self.__clock_opacity)))) x = (width - self.__clock_overlay.sprite.width) // 2 if self.__clock_justify == "L": x *= -1