├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── validate.yml ├── requirements.txt ├── scripts ├── lint ├── setup └── develop ├── hacs.json ├── .gitignore ├── config └── configuration.yaml ├── .vscode └── tasks.json ├── custom_components └── astromech │ ├── const.py │ ├── translations │ └── en.json │ ├── manifest.json │ ├── config_flow.py │ ├── entity.py │ ├── __init__.py │ ├── api.py │ └── tts.py ├── LICENSE ├── .devcontainer.json ├── .ruff.toml ├── README.md └── CONTRIBUTING.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog==6.7.0 2 | homeassistant==2023.5.0 3 | pip>=21.0,<23.2 4 | ruff==0.0.267 5 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | ruff check . --fix 8 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | python3 -m pip install --requirement requirements.txt 8 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Astromech TTS (R2D2 Sounds)", 3 | "filename": "astromech.zip", 4 | "hide_default_branch": true, 5 | "homeassistant": "2023.5.0", 6 | "render_readme": true, 7 | "zip_release": true 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # artifacts 2 | __pycache__ 3 | .pytest* 4 | *.egg-info 5 | */build/* 6 | */dist/* 7 | 8 | 9 | # misc 10 | .coverage 11 | .vscode 12 | coverage.xml 13 | 14 | 15 | # Home Assistant configuration 16 | config/* 17 | !config/configuration.yaml -------------------------------------------------------------------------------- /config/configuration.yaml: -------------------------------------------------------------------------------- 1 | # https://www.home-assistant.io/integrations/default_config/ 2 | default_config: 3 | 4 | # https://www.home-assistant.io/integrations/logger/ 5 | logger: 6 | default: info 7 | logs: 8 | custom_components.astromech: debug 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 8123", 6 | "type": "shell", 7 | "command": "scripts/develop", 8 | "problemMatcher": [] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /custom_components/astromech/const.py: -------------------------------------------------------------------------------- 1 | """Constants for astromech.""" 2 | from logging import Logger, getLogger 3 | 4 | LOGGER: Logger = getLogger(__package__) 5 | 6 | NAME = "Astromech TTS (R2D2 Sounds)" 7 | DOMAIN = "astromech" 8 | VERSION = "0.0.0" 9 | ATTRIBUTION = "" 10 | 11 | VOICE_ASTROMECH = "astromech" 12 | VOICE_ASTROMECH_SHORT = "astromech_short" 13 | -------------------------------------------------------------------------------- /custom_components/astromech/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Astromech", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "title": "Astromech", 7 | "description": "Do you want to configure Astromech?" 8 | } 9 | }, 10 | "abort": { 11 | "already_configured": "Service is already configured" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /custom_components/astromech/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "astromech", 3 | "name": "Astromech TTS (R2D2 Sounds)", 4 | "codeowners": [ 5 | "@vigonotion" 6 | ], 7 | "config_flow": true, 8 | "documentation": "https://github.com/vigonotion/tts.astromech", 9 | "iot_class": "local_polling", 10 | "issue_tracker": "https://github.com/vigonotion/tts.astromech/issues", 11 | "requirements": [ 12 | "ttastromech==0.6.0" 13 | ], 14 | "version": "0.0.0" 15 | } -------------------------------------------------------------------------------- /scripts/develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | # Create config dir if not present 8 | if [[ ! -d "${PWD}/config" ]]; then 9 | mkdir -p "${PWD}/config" 10 | hass --config "${PWD}/config" --script ensure_config 11 | fi 12 | 13 | # Set the path to custom_components 14 | ## This let's us have the structure we want /custom_components/astromech 15 | ## while at the same time have Home Assistant configuration inside /config 16 | ## without resulting to symlinks. 17 | export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" 18 | 19 | # Start Home Assistant 20 | hass --config "${PWD}/config" --debug 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: "Lint" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | ruff: 13 | name: "Ruff" 14 | runs-on: "ubuntu-latest" 15 | steps: 16 | - name: "Checkout the repository" 17 | uses: "actions/checkout@v3.5.2" 18 | 19 | - name: "Set up Python" 20 | uses: actions/setup-python@v4.6.0 21 | with: 22 | python-version: "3.10" 23 | cache: "pip" 24 | 25 | - name: "Install requirements" 26 | run: python3 -m pip install -r requirements.txt 27 | 28 | - name: "Run" 29 | run: python3 -m ruff check . 30 | -------------------------------------------------------------------------------- /custom_components/astromech/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for Blueprint.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant import config_entries 5 | 6 | from .const import DOMAIN 7 | 8 | 9 | class AstromechFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 10 | """Config flow for Blueprint.""" 11 | 12 | VERSION = 1 13 | 14 | async def async_step_user(self, user_input=None): 15 | """Handle a flow initialized by the user.""" 16 | # Check if already configured 17 | await self.async_set_unique_id(DOMAIN) 18 | self._abort_if_unique_id_configured() 19 | 20 | if user_input is not None: 21 | return self.async_create_entry(title="Astromech", data=user_input) 22 | 23 | return self.async_show_form(step_id="user") 24 | -------------------------------------------------------------------------------- /custom_components/astromech/entity.py: -------------------------------------------------------------------------------- 1 | """BlueprintEntity class.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.helpers.entity import DeviceInfo 5 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 6 | 7 | from .const import ATTRIBUTION, DOMAIN, NAME, VERSION 8 | from .coordinator import BlueprintDataUpdateCoordinator 9 | 10 | 11 | class IntegrationBlueprintEntity(CoordinatorEntity): 12 | """BlueprintEntity class.""" 13 | 14 | _attr_attribution = ATTRIBUTION 15 | 16 | def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None: 17 | """Initialize.""" 18 | super().__init__(coordinator) 19 | self._attr_unique_id = coordinator.config_entry.entry_id 20 | self._attr_device_info = DeviceInfo( 21 | identifiers={(DOMAIN, self.unique_id)}, 22 | name=NAME, 23 | model=VERSION, 24 | manufacturer=NAME, 25 | ) 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | release: 5 | types: 6 | - "published" 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | release: 12 | name: "Release" 13 | runs-on: "ubuntu-latest" 14 | permissions: 15 | contents: write 16 | steps: 17 | - name: "Checkout the repository" 18 | uses: "actions/checkout@v3.5.2" 19 | 20 | - name: "Adjust version number" 21 | shell: "bash" 22 | run: | 23 | yq -i -o json '.version="${{ github.event.release.tag_name }}"' \ 24 | "${{ github.workspace }}/custom_components/astromech/manifest.json" 25 | 26 | - name: "ZIP the integration directory" 27 | shell: "bash" 28 | run: | 29 | cd "${{ github.workspace }}/custom_components/astromech" 30 | zip astromech.zip -r ./ 31 | 32 | - name: "Upload the ZIP file to the release" 33 | uses: softprops/action-gh-release@v0.1.15 34 | with: 35 | files: ${{ github.workspace }}/custom_components/astromech/astromech.zip 36 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: "Validate" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: 9 | - "main" 10 | pull_request: 11 | branches: 12 | - "main" 13 | 14 | jobs: 15 | hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest 16 | name: "Hassfest Validation" 17 | runs-on: "ubuntu-latest" 18 | steps: 19 | - name: "Checkout the repository" 20 | uses: "actions/checkout@v3.5.2" 21 | 22 | - name: "Run hassfest validation" 23 | uses: "home-assistant/actions/hassfest@master" 24 | 25 | hacs: # https://github.com/hacs/action 26 | name: "HACS Validation" 27 | runs-on: "ubuntu-latest" 28 | steps: 29 | - name: "Checkout the repository" 30 | uses: "actions/checkout@v3.5.2" 31 | 32 | - name: "Run HACS validation" 33 | uses: "hacs/action@main" 34 | with: 35 | category: "integration" 36 | # Remove this 'ignore' key when you have added brand images for your integration to https://github.com/home-assistant/brands 37 | ignore: "brands" 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - 2023 Joakim Sørensen @ludeeus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /custom_components/astromech/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom integration to integrate astromech with Home Assistant. 2 | 3 | For more details about this integration, please refer to 4 | https://github.com/ludeeus/astromech 5 | """ 6 | from __future__ import annotations 7 | 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import Platform 10 | from homeassistant.core import HomeAssistant 11 | 12 | from .const import DOMAIN 13 | 14 | PLATFORMS: list[Platform] = [Platform.TTS] 15 | 16 | 17 | # https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry 18 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 19 | """Set up this integration using UI.""" 20 | 21 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 22 | entry.async_on_unload(entry.add_update_listener(async_reload_entry)) 23 | 24 | return True 25 | 26 | 27 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 28 | """Handle removal of an entry.""" 29 | if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 30 | hass.data[DOMAIN].pop(entry.entry_id) 31 | return unloaded 32 | 33 | 34 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 35 | """Reload config entry.""" 36 | await async_unload_entry(hass, entry) 37 | await async_setup_entry(hass, entry) 38 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ludeeus/astromech", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye", 4 | "postCreateCommand": "scripts/setup", 5 | "forwardPorts": [ 6 | 8123 7 | ], 8 | "portsAttributes": { 9 | "8123": { 10 | "label": "Home Assistant", 11 | "onAutoForward": "notify" 12 | } 13 | }, 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "ms-python.python", 18 | "github.vscode-pull-request-github", 19 | "ryanluker.vscode-coverage-gutters", 20 | "ms-python.vscode-pylance" 21 | ], 22 | "settings": { 23 | "files.eol": "\n", 24 | "editor.tabSize": 4, 25 | "python.pythonPath": "/usr/bin/python3", 26 | "python.analysis.autoSearchPaths": false, 27 | "python.linting.pylintEnabled": true, 28 | "python.linting.enabled": true, 29 | "python.formatting.provider": "black", 30 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 31 | "editor.formatOnPaste": false, 32 | "editor.formatOnSave": true, 33 | "editor.formatOnType": true, 34 | "files.trimTrailingWhitespace": true 35 | } 36 | } 37 | }, 38 | "remoteUser": "vscode", 39 | "features": { 40 | "ghcr.io/devcontainers/features/rust:1": {} 41 | } 42 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request" 3 | description: "Suggest an idea for this project" 4 | labels: "Feature+Request" 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea. 9 | - type: checkboxes 10 | attributes: 11 | label: Checklist 12 | options: 13 | - label: I have filled out the template to the best of my ability. 14 | required: true 15 | - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). 16 | required: true 17 | - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/ludeeus/astromech/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: "Is your feature request related to a problem? Please describe." 23 | description: "A clear and concise description of what the problem is." 24 | placeholder: "I'm always frustrated when [...]" 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: "Describe the solution you'd like" 31 | description: "A clear and concise description of what you want to happen." 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | attributes: 37 | label: "Describe alternatives you've considered" 38 | description: "A clear and concise description of any alternative solutions or features you've considered." 39 | validations: 40 | required: true 41 | 42 | - type: textarea 43 | attributes: 44 | label: "Additional context" 45 | description: "Add any other context or screenshots about the feature request here." 46 | validations: 47 | required: true 48 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml 2 | 3 | target-version = "py310" 4 | 5 | select = [ 6 | "B007", # Loop control variable {name} not used within loop body 7 | "B014", # Exception handler with duplicate exception 8 | "C", # complexity 9 | "D", # docstrings 10 | "E", # pycodestyle 11 | "F", # pyflakes/autoflake 12 | "ICN001", # import concentions; {name} should be imported as {asname} 13 | "PGH004", # Use specific rule codes when using noqa 14 | "PLC0414", # Useless import alias. Import alias does not rename original package. 15 | "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass 16 | "SIM117", # Merge with-statements that use the same scope 17 | "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() 18 | "SIM201", # Use {left} != {right} instead of not {left} == {right} 19 | "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} 20 | "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. 21 | "SIM401", # Use get from dict with default instead of an if block 22 | "T20", # flake8-print 23 | "TRY004", # Prefer TypeError exception for invalid type 24 | "RUF006", # Store a reference to the return value of asyncio.create_task 25 | "UP", # pyupgrade 26 | "W", # pycodestyle 27 | ] 28 | 29 | ignore = [ 30 | "D202", # No blank lines allowed after function docstring 31 | "D203", # 1 blank line required before class docstring 32 | "D213", # Multi-line docstring summary should start at the second line 33 | "D404", # First word of the docstring should not be This 34 | "D406", # Section name should end with a newline 35 | "D407", # Section name underlining 36 | "D411", # Missing blank line before section 37 | "E501", # line too long 38 | "E731", # do not assign a lambda expression, use a def 39 | ] 40 | 41 | [flake8-pytest-style] 42 | fixture-parentheses = false 43 | 44 | [pyupgrade] 45 | keep-runtime-typing = true 46 | 47 | [mccabe] 48 | max-complexity = 25 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text to Astromech integration for Home Assistant (R2D2 Beep Boop Sounds) 2 | 3 | Generate Astromech sounds in Home Assistant! 4 | 5 | You have the choice between these voices: 6 | 7 | - __Astromech__: Each letter in your TTS message is assigned to a sound. You can check the sounds here: https://github.com/MomsFriendlyRobotCompany/ttastromech/tree/master/ttastromech/sounds 8 | - __Astromech (short)__: The TTS message gets hashed into a 6 letter word, which is then forwarded to the Astromech voice. This helps with long TTS messages that are generated for example by Assist. 9 | 10 | *** 11 | 12 | ## Installation using HACS 13 | 14 | This component is available via HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories) which is the recommended method of installation. 15 | 16 | 1. Add custom repository to HACS 17 | 1. Search for "Astromech" 18 | 1. Install integration 19 | 1. Restart Home Assistant 20 | 1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Astromech TTS" 21 | 22 | ## Manual Installation 23 | 24 | 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). 25 | 1. If you do not have a `custom_components` directory (folder) there, you need to create it. 26 | 1. In the `custom_components` directory (folder) create a new folder called `astromech`. 27 | 1. Download _all_ the files from the `custom_components/astromech/` directory (folder) in this repository. 28 | 1. Place the files you downloaded in the new directory (folder) you created. 29 | 1. Restart Home Assistant 30 | 1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Astromech TTS" 31 | 32 | 33 | ## Contributions are welcome! 34 | 35 | If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) 36 | 37 | *** 38 | 39 | ## Special thanks 40 | 41 | - `ttastromech` for the awesome python library: https://github.com/MomsFriendlyRobotCompany/ttastromech 42 | - https://github.com/hug33k/PyTalk-R2D2 on which `ttastromech` is based on 43 | - Leylosh's Scratch project for the sounds used by the libraries above (https://scratch.mit.edu/projects/766189/) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report" 3 | description: "Report a bug with the integration" 4 | labels: "Bug" 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Before you open a new issue, search through the existing issues to see if others have had the same problem. 9 | - type: textarea 10 | attributes: 11 | label: "System Health details" 12 | description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io//more-info/system-health#github-issues)" 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | attributes: 17 | label: Checklist 18 | options: 19 | - label: I have enabled debug logging for my installation. 20 | required: true 21 | - label: I have filled out the issue template to the best of my ability. 22 | required: true 23 | - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). 24 | required: true 25 | - label: This issue is not a duplicate issue of currently [previous issues](https://github.com/ludeeus/astromech/issues?q=is%3Aissue+label%3A%22Bug%22+).. 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: "Describe the issue" 30 | description: "A clear and concise description of what the issue is." 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Reproduction steps 36 | description: "Without steps to reproduce, it will be hard to fix, it is very important that you fill out this part, issues without it will be closed" 37 | value: | 38 | 1. 39 | 2. 40 | 3. 41 | ... 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: "Debug logs" 47 | description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue." 48 | render: text 49 | validations: 50 | required: true 51 | 52 | - type: textarea 53 | attributes: 54 | label: "Diagnostics dump" 55 | description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)" 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Contributing to this project should be as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Github is used for everything 11 | 12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | Pull requests are the best way to propose changes to the codebase. 15 | 16 | 1. Fork the repo and create your branch from `main`. 17 | 2. If you've changed something, update the documentation. 18 | 3. Make sure your code lints (using `scripts/lint`). 19 | 4. Test you contribution. 20 | 5. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](../../issues) 27 | 28 | GitHub issues are used to track public bugs. 29 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! 30 | 31 | ## Write bug reports with detail, background, and sample code 32 | 33 | **Great Bug Reports** tend to have: 34 | 35 | - A quick summary and/or background 36 | - Steps to reproduce 37 | - Be specific! 38 | - Give sample code if you can. 39 | - What you expected would happen 40 | - What actually happens 41 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 42 | 43 | People *love* thorough bug reports. I'm not even kidding. 44 | 45 | ## Use a Consistent Coding Style 46 | 47 | Use [black](https://github.com/ambv/black) to make sure the code follows the style. 48 | 49 | ## Test your code modification 50 | 51 | This custom component is based on [astromech template](https://github.com/ludeeus/astromech). 52 | 53 | It comes with development environment in a container, easy to launch 54 | if you use Visual Studio Code. With this container you will have a stand alone 55 | Home Assistant instance running and already configured with the included 56 | [`configuration.yaml`](./config/configuration.yaml) 57 | file. 58 | 59 | ## License 60 | 61 | By contributing, you agree that your contributions will be licensed under its MIT License. 62 | -------------------------------------------------------------------------------- /custom_components/astromech/api.py: -------------------------------------------------------------------------------- 1 | """Sample API Client.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | import socket 6 | 7 | import aiohttp 8 | import async_timeout 9 | 10 | 11 | class IntegrationBlueprintApiClientError(Exception): 12 | """Exception to indicate a general API error.""" 13 | 14 | 15 | class IntegrationBlueprintApiClientCommunicationError( 16 | IntegrationBlueprintApiClientError 17 | ): 18 | """Exception to indicate a communication error.""" 19 | 20 | 21 | class IntegrationBlueprintApiClientAuthenticationError( 22 | IntegrationBlueprintApiClientError 23 | ): 24 | """Exception to indicate an authentication error.""" 25 | 26 | 27 | class IntegrationBlueprintApiClient: 28 | """Sample API Client.""" 29 | 30 | def __init__( 31 | self, 32 | username: str, 33 | password: str, 34 | session: aiohttp.ClientSession, 35 | ) -> None: 36 | """Sample API Client.""" 37 | self._username = username 38 | self._password = password 39 | self._session = session 40 | 41 | async def async_get_data(self) -> any: 42 | """Get data from the API.""" 43 | return await self._api_wrapper( 44 | method="get", url="https://jsonplaceholder.typicode.com/posts/1" 45 | ) 46 | 47 | async def async_set_title(self, value: str) -> any: 48 | """Get data from the API.""" 49 | return await self._api_wrapper( 50 | method="patch", 51 | url="https://jsonplaceholder.typicode.com/posts/1", 52 | data={"title": value}, 53 | headers={"Content-type": "application/json; charset=UTF-8"}, 54 | ) 55 | 56 | async def _api_wrapper( 57 | self, 58 | method: str, 59 | url: str, 60 | data: dict | None = None, 61 | headers: dict | None = None, 62 | ) -> any: 63 | """Get information from the API.""" 64 | try: 65 | async with async_timeout.timeout(10): 66 | response = await self._session.request( 67 | method=method, 68 | url=url, 69 | headers=headers, 70 | json=data, 71 | ) 72 | if response.status in (401, 403): 73 | raise IntegrationBlueprintApiClientAuthenticationError( 74 | "Invalid credentials", 75 | ) 76 | response.raise_for_status() 77 | return await response.json() 78 | 79 | except asyncio.TimeoutError as exception: 80 | raise IntegrationBlueprintApiClientCommunicationError( 81 | "Timeout error fetching information", 82 | ) from exception 83 | except (aiohttp.ClientError, socket.gaierror) as exception: 84 | raise IntegrationBlueprintApiClientCommunicationError( 85 | "Error fetching information", 86 | ) from exception 87 | except Exception as exception: # pylint: disable=broad-except 88 | raise IntegrationBlueprintApiClientError( 89 | "Something really wrong happened!" 90 | ) from exception 91 | -------------------------------------------------------------------------------- /custom_components/astromech/tts.py: -------------------------------------------------------------------------------- 1 | """Astromech TTS component.""" 2 | 3 | import io 4 | from typing import Any 5 | from homeassistant.components import tts 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 9 | from homeassistant.components.tts.models import Voice 10 | 11 | import re 12 | import random 13 | import string 14 | import wave 15 | import hashlib 16 | 17 | from ttastromech import TTAstromech 18 | 19 | from .const import VOICE_ASTROMECH, VOICE_ASTROMECH_SHORT 20 | 21 | 22 | def generate_hash(input, length): 23 | """Generate hash.""" 24 | md5_hash = hashlib.md5(input.encode()).hexdigest() 25 | hash_length = len(md5_hash) 26 | ratio = hash_length / length 27 | result = "" 28 | 29 | for i in range(length): 30 | start = int(i * ratio) 31 | end = int((i + 1) * ratio) 32 | substring = md5_hash[start:end] 33 | ascii_sum = sum(ord(c) for c in substring) 34 | char_index = ascii_sum % 26 35 | char = chr(ord("a") + char_index) 36 | result += char 37 | 38 | return result 39 | 40 | 41 | def replace_non_alpha_chars(text): 42 | """Replace invalid characters with a random lowercase ascii letter.""" 43 | 44 | random_char = lambda: random.choice(string.ascii_lowercase) 45 | 46 | replaced_text = re.sub(r"[^a-z]", lambda match: random_char(), text) 47 | return replaced_text 48 | 49 | 50 | async def async_setup_entry( 51 | hass: HomeAssistant, 52 | config_entry: ConfigEntry, 53 | async_add_entities: AddEntitiesCallback, 54 | ) -> None: 55 | """Set up Wyoming speech to text.""" 56 | async_add_entities( 57 | [ 58 | TextToAstromech(), 59 | ] 60 | ) 61 | 62 | 63 | class TextToAstromech(tts.TextToSpeechEntity): 64 | """Represent a Text To Speech entity.""" 65 | 66 | def __init__(self) -> None: 67 | """Init.""" 68 | self._r2 = TTAstromech() 69 | 70 | self._attr_name = "Astromech" 71 | self._attr_unique_id = "astromech-tts" 72 | 73 | @property 74 | def supported_languages(self) -> list[str]: 75 | """Return a list of supported languages.""" 76 | return ["en", "de", "pl", "es", "it", "fr", "pt", "hi"] # TODO return all 77 | 78 | @property 79 | def default_language(self) -> str: 80 | """Returns the default language.""" 81 | return "en" 82 | 83 | @property 84 | def supported_options(self) -> list[str]: 85 | """Return a list of supported options.""" 86 | return [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE] 87 | 88 | @property 89 | def default_options(self): 90 | """Return a dict include default options.""" 91 | return {tts.ATTR_AUDIO_OUTPUT: "wav"} 92 | 93 | @callback 94 | def async_get_supported_voices(self, language: str) -> list[Voice] | None: 95 | """Return a list of supported voices for a language.""" 96 | return [ 97 | Voice(VOICE_ASTROMECH, "Astromech"), 98 | Voice(VOICE_ASTROMECH_SHORT, "Astromech (short)"), 99 | ] 100 | 101 | def get_tts_audio( 102 | self, message: str, language: str, options: dict[str, Any] 103 | ) -> tts.TtsAudioType: 104 | """Load tts audio file from the engine.""" 105 | slug = replace_non_alpha_chars(message.lower()) 106 | 107 | if options.get(tts.ATTR_VOICE) == VOICE_ASTROMECH_SHORT: 108 | slug = generate_hash(message.lower(), 6) 109 | 110 | data = self._r2.generate(slug) 111 | 112 | output_stream = io.BytesIO() 113 | output_file = wave.open(output_stream, "wb") 114 | output_file.setnchannels(1) # mono 115 | output_file.setsampwidth(2) # 16 bits 116 | output_file.setframerate(22050) # Hz 117 | output_file.writeframes(data) 118 | output_file.close() 119 | 120 | # Read the content of the byte stream into a byte array 121 | byte_array = output_stream.getvalue() 122 | 123 | if options[tts.ATTR_AUDIO_OUTPUT] == "wav": 124 | return ("wav", byte_array) 125 | return ("raw", byte_array) --------------------------------------------------------------------------------