first commit

This commit is contained in:
2025-10-26 14:46:43 +01:00
commit 4a6f844a87
8 changed files with 762 additions and 0 deletions

110
.gitignore vendored Normal file
View File

@ -0,0 +1,110 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

213
README.md Normal file
View File

@ -0,0 +1,213 @@
# Strike Bitcoin Sensor for Home Assistant
A custom Home Assistant integration that fetches Bitcoin price data from the Strike API and creates sensor entities for monitoring cryptocurrency prices.
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
## Features
- 🔄 Real-time Bitcoin (BTC) price tracking from Strike API
- 💱 Configurable currency pairs (default: BTC/USD)
- ⏱️ Customizable update intervals
- 🔐 Secure API key storage
- 🎨 Home Assistant UI configuration flow
- 📊 Additional state attributes (currency, source, last update)
- 🛡️ Comprehensive error handling and logging
- ✨ Follows Home Assistant development best practices
## Installation
### HACS (Recommended)
1. Open HACS in your Home Assistant instance
2. Click on "Integrations"
3. Click the three dots in the top right corner
4. Select "Custom repositories"
5. Add the repository URL: `https://github.com/yourusername/hass-to-be-good`
6. Select category: "Integration"
7. Click "Add"
8. Search for "Strike Bitcoin" in HACS
9. Click "Download"
10. Restart Home Assistant
### Manual Installation
1. Copy the `custom_components/strike` folder to your Home Assistant `custom_components` directory
2. Restart Home Assistant
## Configuration
### Prerequisites
You need a Strike API key to use this integration:
1. Visit [Strike](https://strike.me/) and create an account
2. Navigate to the API section in your Strike dashboard
3. Generate a new API key
4. Copy the API key for use in Home Assistant
### Setup via UI
1. Go to **Settings****Devices & Services**
2. Click **+ Add Integration**
3. Search for **Strike Bitcoin**
4. Enter your Strike API key
5. (Optional) Configure currency pair (default: BTCUSD)
6. (Optional) Set update interval in seconds (default: 300 seconds / 5 minutes)
7. Click **Submit**
## Usage
Once configured, the integration will create a sensor entity:
- **Entity ID**: `sensor.strike_btcusd`
- **State**: Current Bitcoin price
- **Unit**: Currency (e.g., USD)
- **Icon**: `mdi:bitcoin`
### State Attributes
The sensor provides additional information as attributes:
- `currency`: Target currency (e.g., USD)
- `source`: Currency pair (e.g., BTC/USD)
- `last_update`: Timestamp of last update
### Example Automation
```yaml
automation:
- alias: "Bitcoin Price Alert"
trigger:
- platform: numeric_state
entity_id: sensor.strike_btcusd
above: 50000
action:
- service: notify.notify
data:
message: "Bitcoin price is above $50,000!"
```
### Example Lovelace Card
```yaml
type: entities
entities:
- entity: sensor.strike_btcusd
name: Bitcoin Price
icon: mdi:bitcoin
```
## Configuration Options
### Options Flow
You can modify the integration settings after setup:
1. Go to **Settings****Devices & Services**
2. Find **Strike Bitcoin** integration
3. Click **Configure**
4. Adjust the update interval
5. Click **Submit**
## Troubleshooting
### Integration Not Loading
- Check Home Assistant logs for errors
- Verify API key is correct
- Ensure you have internet connectivity
- Restart Home Assistant
### API Errors
Common errors and solutions:
- **401 Unauthorized**: Invalid API key - regenerate in Strike dashboard
- **403 Forbidden**: API key doesn't have required permissions
- **Connection timeout**: Check network connectivity or increase timeout in `const.py`
### Enable Debug Logging
Add to your `configuration.yaml`:
```yaml
logger:
default: info
logs:
custom_components.strike: debug
```
## Development
### Repository Structure
```
hass-to-be-good/
├── custom_components/
│ └── strike/
│ ├── __init__.py # Integration setup
│ ├── config_flow.py # Configuration UI
│ ├── const.py # Constants and configuration
│ ├── manifest.json # Integration metadata
│ └── sensor.py # Sensor implementation
├── .gitignore
├── hacs.json # HACS configuration
└── README.md
```
### Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
### Testing
Before submitting changes:
1. Test the integration in a Home Assistant development environment
2. Verify configuration flow works correctly
3. Check logs for errors or warnings
4. Ensure code follows Home Assistant style guidelines
## Strike API Reference
This integration uses the Strike API v1:
- **Base URL**: `https://api.strike.me`
- **Endpoint**: `/v1/rates/ticker`
- **Authentication**: Bearer token (API key)
- **Documentation**: [Strike API Docs](https://docs.strike.me/)
## License
This project is provided as-is for educational and personal use.
## Support
For issues, questions, or feature requests:
- Open an issue on [GitHub](https://github.com/yourusername/hass-to-be-good/issues)
- Check existing issues for solutions
- Provide Home Assistant logs when reporting bugs
## Changelog
### Version 1.0.0
- Initial release
- Bitcoin price sensor with Strike API integration
- UI configuration flow
- Configurable update intervals
- Secure API key storage
- Comprehensive error handling
## Acknowledgments
- Built for [Home Assistant](https://www.home-assistant.io/)
- Uses [Strike API](https://strike.me/)
- Follows [Home Assistant integration development guidelines](https://developers.home-assistant.io/)

View File

@ -0,0 +1,47 @@
"""The Strike Bitcoin integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Strike Bitcoin from a config entry."""
_LOGGER.debug("Setting up Strike Bitcoin integration")
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
"session": async_get_clientsession(hass),
"config": entry.data,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
_LOGGER.debug("Unloading Strike Bitcoin integration")
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View File

@ -0,0 +1,164 @@
"""Config flow for Strike Bitcoin integration."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import (
API_BASE_URL,
API_TIMEOUT,
CONF_API_KEY,
CONF_CURRENCY_PAIR,
DEFAULT_CURRENCY_PAIR,
DEFAULT_NAME,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
TICKER_ENDPOINT,
)
_LOGGER = logging.getLogger(__name__)
async def validate_api_key(hass: HomeAssistant, api_key: str, currency_pair: str) -> dict[str, Any]:
"""Validate the API key by making a test request."""
session = async_get_clientsession(hass)
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {api_key}",
}
try:
async with session.get(
f"{API_BASE_URL}{TICKER_ENDPOINT}",
headers=headers,
params={"sourceCurrency": currency_pair[:3], "targetCurrency": currency_pair[3:]},
timeout=aiohttp.ClientTimeout(total=API_TIMEOUT),
) as response:
if response.status == 401:
raise InvalidAuth
if response.status == 403:
raise InvalidAuth
if response.status != 200:
_LOGGER.error("API request failed with status %s", response.status)
raise CannotConnect
data = await response.json()
_LOGGER.debug("API validation successful: %s", data)
return {"title": f"{DEFAULT_NAME} ({currency_pair})"}
except aiohttp.ClientError as err:
_LOGGER.error("Error connecting to Strike API: %s", err)
raise CannotConnect from err
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error validating API key: %s", err)
raise CannotConnect from err
class StrikeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Strike Bitcoin."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
info = await validate_api_key(
self.hass,
user_input[CONF_API_KEY],
user_input.get(CONF_CURRENCY_PAIR, DEFAULT_CURRENCY_PAIR),
)
await self.async_set_unique_id(
f"{user_input.get(CONF_CURRENCY_PAIR, DEFAULT_CURRENCY_PAIR)}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info["title"],
data=user_input,
)
except InvalidAuth:
errors["base"] = "invalid_auth"
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
data_schema = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Optional(
CONF_CURRENCY_PAIR, default=DEFAULT_CURRENCY_PAIR
): str,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
}
)
return self.async_show_form(
step_id="user",
data_schema=data_schema,
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> StrikeOptionsFlowHandler:
"""Get the options flow for this handler."""
return StrikeOptionsFlowHandler(config_entry)
class StrikeOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Strike options."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): cv.positive_int,
}
),
)
class CannotConnect(Exception):
"""Error to indicate we cannot connect."""
class InvalidAuth(Exception):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,29 @@
"""Constants for the Strike Bitcoin integration."""
from typing import Final
DOMAIN: Final = "strike"
# API Configuration
API_BASE_URL: Final = "https://api.strike.me"
API_TIMEOUT: Final = 10
# Strike API Endpoints
TICKER_ENDPOINT: Final = "/v1/rates/ticker"
# Configuration
CONF_API_KEY: Final = "api_key"
CONF_CURRENCY_PAIR: Final = "currency_pair"
# Default values
DEFAULT_NAME: Final = "Strike Bitcoin"
DEFAULT_CURRENCY_PAIR: Final = "BTCUSD"
DEFAULT_SCAN_INTERVAL: Final = 300 # 5 minutes
# Attributes
ATTR_CURRENCY: Final = "currency"
ATTR_LAST_UPDATE: Final = "last_update"
ATTR_SOURCE: Final = "source"
# Device info
MANUFACTURER: Final = "Strike"
MODEL: Final = "Bitcoin Price Ticker"

View File

@ -0,0 +1,11 @@
{
"domain": "strike",
"name": "Strike Bitcoin",
"codeowners": ["@martien"],
"config_flow": true,
"documentation": "https://github.com/martien/hass-to-be-good",
"issue_tracker": "https://github.com/martien/hass-to-be-good/issues",
"requirements": ["aiohttp>=3.8.0"],
"version": "1.0.0",
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,181 @@
"""Support for Strike Bitcoin price sensor."""
from __future__ import annotations
from datetime import datetime, timedelta
import logging
from typing import Any
import aiohttp
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import (
API_BASE_URL,
API_TIMEOUT,
ATTR_CURRENCY,
ATTR_LAST_UPDATE,
ATTR_SOURCE,
CONF_API_KEY,
CONF_CURRENCY_PAIR,
DEFAULT_CURRENCY_PAIR,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
MANUFACTURER,
MODEL,
TICKER_ENDPOINT,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Strike Bitcoin sensor based on a config entry."""
coordinator = StrikeBitcoinCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
async_add_entities([StrikeBitcoinSensor(coordinator, entry)])
class StrikeBitcoinCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Strike Bitcoin data."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
self.api_key = entry.data[CONF_API_KEY]
self.currency_pair = entry.data.get(CONF_CURRENCY_PAIR, DEFAULT_CURRENCY_PAIR)
self.session = hass.data[DOMAIN][entry.entry_id]["session"]
scan_interval = entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=scan_interval),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from Strike API."""
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
# Parse currency pair (e.g., BTCUSD -> BTC and USD)
source_currency = self.currency_pair[:3]
target_currency = self.currency_pair[3:]
try:
async with self.session.get(
f"{API_BASE_URL}{TICKER_ENDPOINT}",
headers=headers,
params={
"sourceCurrency": source_currency,
"targetCurrency": target_currency,
},
timeout=aiohttp.ClientTimeout(total=API_TIMEOUT),
) as response:
if response.status == 401:
raise UpdateFailed("Invalid API key")
if response.status == 403:
raise UpdateFailed("API key forbidden")
if response.status != 200:
raise UpdateFailed(f"API request failed with status {response.status}")
data = await response.json()
# Strike API returns the ticker data
# Expected format: {"amount": "50000.00"}
if "amount" not in data:
raise UpdateFailed("Invalid response format from Strike API")
return {
"price": float(data["amount"]),
"currency": target_currency,
"source_currency": source_currency,
"last_update": datetime.now().isoformat(),
}
except aiohttp.ClientError as err:
_LOGGER.error("Error connecting to Strike API: %s", err)
raise UpdateFailed(f"Error connecting to Strike API: {err}") from err
except Exception as err:
_LOGGER.exception("Unexpected error fetching Strike data: %s", err)
raise UpdateFailed(f"Unexpected error: {err}") from err
class StrikeBitcoinSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Strike Bitcoin Price Sensor."""
_attr_has_entity_name = True
_attr_name = None
_attr_device_class = SensorDeviceClass.MONETARY
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:bitcoin"
def __init__(
self, coordinator: StrikeBitcoinCoordinator, entry: ConfigEntry
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{entry.entry_id}_btc_price"
self._entry_id = entry.entry_id
self.currency_pair = entry.data.get(CONF_CURRENCY_PAIR, DEFAULT_CURRENCY_PAIR)
# Set device info
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.entry_id)},
"name": f"Strike {self.currency_pair}",
"manufacturer": MANUFACTURER,
"model": MODEL,
"entry_type": "service",
}
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
if self.coordinator.data is None:
return None
return self.coordinator.data.get("price")
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self.coordinator.data is None:
return None
return self.coordinator.data.get("currency", "USD")
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
if self.coordinator.data is None:
return {}
return {
ATTR_CURRENCY: self.coordinator.data.get("currency"),
ATTR_SOURCE: f"{self.coordinator.data.get('source_currency')}/{self.coordinator.data.get('currency')}",
ATTR_LAST_UPDATE: self.coordinator.data.get("last_update"),
}
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.coordinator.last_update_success and self.coordinator.data is not None

7
hacs.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "Strike Bitcoin Sensor",
"content_in_root": false,
"filename": "strike",
"render_readme": true,
"homeassistant": "2023.1.0"
}