diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9bea433
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+
+.DS_Store
diff --git a/README.md b/README.md
index 2bc04dc..bafe690 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Package-Notification-HUB-AppDaemon
The app works with Appdaemon v4.x.
## Installation
-Use [HACS](https://github.com/custom-components/hacs) or [download](https://github.com/jumping2000/notifier) the `notifier` directory from inside the `apps` directory here to your local `apps` directory.
+Use [HACS](https://github.com/custom-components/hacs) or [download](https://github.com/jumping2000/notifier) the `notifier` directory from inside the `apps` directory here to your local `apps` directory.
Remember to enable [Appdaemon in HACS](https://hacs.xyz/docs/categories/appdaemon_apps#enable-appdaemon-apps-in-hacs).
## Remember it!
diff --git a/apps/notifier/alexa_manager.py b/apps/notifier/alexa_manager.py
old mode 100644
new mode 100755
index 99796a2..60ca681
--- a/apps/notifier/alexa_manager.py
+++ b/apps/notifier/alexa_manager.py
@@ -1,93 +1,66 @@
-import hassapi as hass
-
-import re
-import sys
-import time
+import json
from queue import Queue
+import re
from threading import Thread
+import time
+
+import hassapi as hass # type: ignore
"""
Class Alexa Manager handles sending text to speech messages to Alexa media players
Following features are implemented:
- - Speak text to choosen media_player
+ - Speak text to choosen media player
- Full queue support to manage async multiple TTS commands
- Full wait to tts to finish to be able to supply a callback method
+ - Mobile PUSH message
+ - Media content
+ - SSML, language, voice
+ - Alexa Actions: Actionable Notification
"""
+ALEXA_SERVICE = "alexa_media"
+CUSTOM_COMPONENT_URL = "https://github.com/custom-components/alexa_media_player"
NOTIFY = "notify/"
-
+SKILL_ID = "skill_id"
+DEFAULT_VOL = "default_vol"
+
+# Parameters
+TITLE = "title"
+MESSAGE = "message"
+AUDIO = "audio"
+AUTO_VOLUMES = "auto_volumes"
+EVENT_ID = "event_id"
+LANGUAGE = "language"
+MEDIA_CONTENT_ID = "media_content_id"
+MEDIA_CONTENT_TYPE = "media_content_type"
+MEDIA_PLAYER = "media_player"
+METHOD = "method"
+NOTIFIER = "notifier"
+PITCH = "pitch"
+PUSH = "push"
+RATE = "rate"
+SSML = "ssml"
+SSML_VOL = "ssml_volume"
+TYPE = "type"
+VOICE = "voice"
+VOLUME = "volume"
+WAIT_TIME = "wait_time"
+WHISPER = "whisper"
+# MODE = "mode"
+# PRIORITY = "priority"
+
+MOBILE_PUSH_TYPE = (PUSH, "dropin", "dropin_notification")
SUB_VOICE = [
- # ("[.]{2,}", "."),
- ("[\?\.\!,]+(?=[\?\.\!,])", ""), # Exclude duplicate
- ("(\s+\.|\s+\.\s+|[\.])(?! )(?![^{]*})(?![^\d.]*\d)", ". "),
+ ("[\U00010000-\U0010ffff]", ""), # strip emoji
+ ("[\?\.\!,]+(?=[\?\.\!,])", ""), # strip duplicate .,
+ ("(\s+\.|\s+\.\s+|[\.])(?! )(?![^{<]*[}>])(?![^\d.]*\d)", ". "),
("&", " and "), # escape
- # ("(? None:
- # self.set_log_level("DEBUG")
- self.alexa_service = self.args.get("alexa_service")
- # self.alexa_switch_entity = self.args.get("alexa_switch")
- self.alexa_select_media_player = self.args.get("alexa_select_media_player")
- self.alexa_type = self.args.get("alexa_type")
- self.alexa_method = self.args.get("alexa_method")
- self.alexa_sensor_media_player = self.args.get("alexa_sensor_media_player")
- self.alexa_voice = self.args.get("alexa_voice")
- # self.alexa_language = self.args.get("alexa_language")
+ self.debug_sensor = self.args.get("debug_sensor")
+ self.component_installed = self.is_component_installed(ALEXA_SERVICE)
+ self.notify_services = self.list_notify_services(ALEXA_SERVICE)
+ self.service2player = self.alexa_services_to_players(self.notify_services)
+ self.volumes_saved = {} # dict media players volume saved
+ # Entities
+ self.binary_speak = self.args.get("binary_speak")
+ self.sensor_player = self.args.get("sensor_player")
+ self.sensor_volume = self.args.get("sensor_day_volume")
+ self.select_language = self.args.get("select_language")
+ self.select_alexa_language = self.args.get("select_alexa_language")
+ self.select_player = self.args.get("select_player")
+ self.select_type = self.args.get("select_type")
+ self.select_method = self.args.get("select_method")
+ self.select_voice = self.args.get("select_voice")
+ self.bool_smart_volume_set = self.args.get("bool_smart_volume_set")
+ self.bool_ssml = self.args.get("bool_ssml")
self.prosody = self.args.get("prosody")
- self.wait_time = self.args.get("wait_time")
- self.cehck_alexa_service = self._check_alexa(self.alexa_service)
+ self.number_wait_time = self.args.get("number_wait_time")
+ self.text_actionable_notification = self.args.get("actionable_notification")
self.queue = Queue(maxsize=0)
self._when_tts_done_callback_queue = Queue()
-
t = Thread(target=self.worker)
t.daemon = True
t.start()
+ self.set_state(self.debug_sensor, state="on")
- def speak(self, alexa):
+ def speak(self, alexa: dict, skill_id: str) -> None:
"""Speak the provided text through the media player."""
- if not self.cehck_alexa_service:
- self.set_sensor(
- "I can't find the Alexa Media component", "https://github.com/custom-components/alexa_media_player"
- )
- return
- self.lg(f"-------------------- ALEXA START DISPATCH --------------------")
+ self.lg(f"------ ALEXA START DISPATCH ------")
self.lg(f"FROM DISPATCH: {type(alexa)} value {alexa}")
- # remove keys with None value from a dict # TODO
- alexa = {k: v for k, v in alexa.items() if v not in [None, "None", ""]}
- self.lg(f"REMOVE [NONE] VALUE: {type(alexa)} value {alexa}")
- default_restore_volume = float(self.get_state(self.args.get("default_restore_volume"))) / 100
- volume = float(alexa.get("volume", default_restore_volume))
- message = str(alexa.get("message", alexa.get("message_tts", "")))
- alexa_player = self.player_get(alexa.get("media_player", self.get_state(self.alexa_sensor_media_player)))
- alexa_type = (
- str(alexa.get("type", self.get_state(self.alexa_type))).lower().replace("dropin", "dropin_notification")
- )
- # Push notification
- push = bool(self.check_bool(alexa.get("push")))
- if push or alexa_type in MOBILE_PUSH and message:
+ if not self.service2player:
+ self.set_debug_sensor("Alexa Services not found", CUSTOM_COMPONENT_URL)
+ return
+
+ default_vol = float(self.get_state(self.sensor_volume, default=10)) / 100
+ volume = float(alexa.get(VOLUME, default_vol))
+ auto_volumes = self.check_bool(alexa.get(AUTO_VOLUMES, False))
+ if volume == 0.0 and not auto_volumes:
+ self.log("ALEXA VOLUME MUTED.", level="WARNING")
+ return
+
+ # Backwards compatible message_tts
+ message = str(alexa.get("message_tts", alexa.get(MESSAGE, "")))
+ get_players = alexa.get(MEDIA_PLAYER, self.get_state(self.sensor_player))
+ media_player = self.check_media_player(get_players)
+ get_type = alexa.get(TYPE, self.get_state(self.select_type, default="tts"))
+ data_type = str(get_type).lower().replace("dropin", "dropin_notification")
+
+ alexa_lang = self.get_state(self.select_alexa_language, default="Master")
+ master_lang = self.get_state(self.select_language, default="it-IT")
+ final_language = alexa_lang if alexa_lang != "Master" else master_lang
+ language = str(alexa.get(LANGUAGE, final_language))
+
+ state_method = self.get_state(self.select_method, default="all")
+ state_voice = self.get_state(self.select_voice, default="Alexa")
+ state_wait_time = self.get_state(self.number_wait_time, default=3.0)
+ state_rate = self.get_state(self.prosody[RATE], default=100.0)
+ state_pitch = self.get_state(self.prosody[PITCH], default=0.0)
+ state_ssml_volume = self.get_state(self.prosody[VOLUME], default=0.0)
+ state_ssml = self.get_state(self.bool_ssml, default="off")
+
+ # Actionable notification
+ if event_id := alexa.get(EVENT_ID, ""):
+ self.set_textvalue(
+ self.text_actionable_notification,
+ json.dumps({"text": "", "event": event_id}),
+ )
+
+ # Push notification - Only one device is needed
+ push = self.check_bool(alexa.get(PUSH, False))
+ if (push or data_type in MOBILE_PUSH_TYPE) and message:
message_push = self.remove_tags(self.replace_regular(message, SUB_TEXT))
+ type_ = {TYPE: PUSH} if push else {TYPE: data_type}
self.call_service(
- NOTIFY + self.alexa_service,
- data={"type": "push"} if push else {"type": alexa_type},
- target=alexa_player[0], # only one device
- title=str(alexa.get("title", "")),
+ NOTIFY + ALEXA_SERVICE,
+ data=type_,
+ target=media_player[0],
+ title=str(alexa.get(TITLE, "")),
message=message_push,
)
- self.lg(f"PUSH: {push} - TYPE: {alexa_type} - MESSAGE: {message_push}")
+
# Media Content # TODO Restore volume??
- media_content_id = alexa.get("media_content_id")
- media_content_type = alexa.get("media_content_type")
- if media_content_id:
- self.volume_get(alexa_player, default_restore_volume)
- self.volume_set(alexa_player, volume)
+ if media_content_id := alexa.get(MEDIA_CONTENT_ID):
+ self.volume_get_save(media_player, volume, default_vol)
+ self.volume_set(media_player, volume)
self.call_service(
"media_player/play_media",
- entity_id=alexa_player,
+ entity_id=media_player,
media_content_id=media_content_id,
- media_content_type=media_content_type,
- # extra = {"timer": 10} ##??
+ media_content_type=alexa.get(MEDIA_CONTENT_TYPE),
+ extra={"timer": alexa.get("extra", 0)},
)
- self.lg(f"Content id: {media_content_id} - Content type: {media_content_type}")
- # Queues the message to be handled async, use when_tts_done_do method to supply callback when tts is done
- elif alexa_type not in MOBILE_PUSH and message:
+ # Queues the message to be handled async, use when_tts_done_do method
+ # to supply callback when tts is done
+ elif data_type not in MOBILE_PUSH_TYPE and message:
self.queue.put(
{
- "text": message,
- "volume": volume,
- "alexa_type": alexa_type,
- "alexa_player": alexa_player, # media_player
- "default_restore_volume": default_restore_volume,
- "alexa_notifier": str(alexa.get("notifier", self.alexa_service)),
- "wait_time": float(alexa.get("wait_time", self.get_state(self.wait_time))),
- "language": alexa.get("language"), # self.get_state(self.alexa_language)),
- "alexa_method": str(alexa.get("method", self.get_state(self.alexa_method)).lower()),
- "alexa_voice": str(alexa.get("voice", self.get_state(self.alexa_voice))).capitalize(),
- "alexa_audio": alexa.get("audio", None),
- "rate": float(alexa.get("rate", self.get_state(self.prosody["rate"]))),
- "pitch": float(alexa.get("pitch", self.get_state(self.prosody["pitch"]))),
- "ssml_volume": float(alexa.get("ssml_volume", self.get_state(self.prosody["volume"]))),
- "whisper": bool(self.check_bool(alexa.get("whisper", False))),
- "ssml_switch": bool(self.check_bool(alexa.get("ssml", self.get_state(self.args["ssml_switch"])))),
+ SKILL_ID: skill_id,
+ DEFAULT_VOL: default_vol,
+ VOLUME: volume,
+ AUTO_VOLUMES: auto_volumes,
+ MESSAGE: message,
+ MEDIA_PLAYER: media_player,
+ TYPE: data_type,
+ LANGUAGE: language,
+ EVENT_ID: event_id,
+ AUDIO: alexa.get(AUDIO, None),
+ NOTIFIER: str(alexa.get(NOTIFIER, ALEXA_SERVICE)),
+ METHOD: str(alexa.get(METHOD, state_method).lower()),
+ VOICE: str(alexa.get(VOICE, state_voice)).capitalize(),
+ WAIT_TIME: float(alexa.get(WAIT_TIME, state_wait_time)),
+ RATE: float(alexa.get(RATE, state_rate)),
+ PITCH: float(alexa.get(PITCH, state_pitch)),
+ SSML_VOL: float(alexa.get(SSML_VOL, state_ssml_volume)),
+ WHISPER: self.check_bool(alexa.get(WHISPER, False)),
+ SSML: self.check_bool(alexa.get(SSML, state_ssml)),
}
)
- self.lg(f"-------------------- ALEXA END DISPATCH --------------------")
- def lg(self, message):
- self.log(message, level="DEBUG", ascii_encode=False)
+ def lg(self, message: str) -> None:
+ self.log(str(message), level="DEBUG", ascii_encode=False)
- def check_bool(self, value):
+ def check_bool(self, value) -> bool:
+ """Check if user input is a boolean."""
return str(value).lower() in ["true", "on", "yes", "1"]
- def inbetween(self, minv, value, maxv):
+ def inbetween(self, minv: float, value: float, maxv: float) -> float:
+ """Check input number between minimum and maximum values range."""
return sorted([minv, value, maxv])[1]
- def speak_tag(self, value): # TODO tags
- return value if "" in value or not "" in value else f"{value}"
+ def replace_regular(self, text: str, substitutions: list) -> str:
+ for old, new in substitutions:
+ regex = re.compile(old)
+ text = re.sub(regex, new, str(text).strip())
+ return text
+
+ def str2list(self, string: str) -> list:
+ """Convert string to list."""
+ regex = re.compile(r"\s*,\s*")
+ return self.split_device_list(re.sub(regex, ",", string))
+
+ def has_numbers(self, string: str):
+ """Check if a string contains a number."""
+ numbers = re.compile("\d{2}:\d{2}|\d{4,}|\d{3,}\.\d")
+ return numbers.search(string)
- def effect_tag(self, value):
+ def remove_tags(self, text: str) -> str:
+ """Remove all tags from a string."""
+ regex = re.compile("<.*?>")
+ return re.sub(regex, "", str(text).strip())
+
+ def speak_tags(self, value: str) -> str:
+ """This will add a tag when using tts method"""
+ return f"{value}"
+
+ def effect_tags(self, value: str) -> str:
+ """This will add a tag and applies a whispering effect."""
return f"{value}"
- def prosody_tag(self, value, rate, pitch, volume):
+ def prosody_tags(self, value: str, rate: float, pitch: float, volume: float) -> str:
+ """This will add a tag for volume, pitch, and rate"""
if rate != 100.0 or pitch != 0.0 or volume != 0.0:
- rate = f"{self.inbetween(20, rate, 200)}%" # min 20% max 200%
- pitch = f"{self.inbetween(-33.3, pitch, 50):+g}%" # min -33.3 max +50
- volume = f"{self.inbetween(-50, volume, 4.08):+g}dB" # max +4.08dB
- return f" {value} "
+ r = f"{self.inbetween(20, rate, 200)}%"
+ p = f"{self.inbetween(-33.3, pitch, 50):+g}%"
+ v = f"{self.inbetween(-50, volume, 4.08):+g}dB"
+ return f" {value} "
return value
- def audio_tag(self, value: None):
+ def audio_tags(self, value: None) -> str:
+ """This will add the