diff --git a/README.md b/README.md index 9a7d752..79ef190 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,25 @@ alt="Mobile Vikings" align="right" style="width: 200px;margin-right: 10px;" /> +Jim Mobile -# Mobile Vikings for Home Assistant +# Mobile Vikings & JimMobile mobile phone subscription information for Home Assistant -A Home Assistant integration to monitor Mobile Vikings BE services +A Home Assistant integration to monitor Mobile Vikings or Jim Mobile BE mobile phone subscription services ## Features - View **Customer Info** such as name and account details. -- Track **Loyalty Points**: Available, blocked, and pending points. -- Monitor **Invoices**: Paid invoices, unpaid invoices, and the next expiration date. +- Track **Loyalty Points**: Available, blocked, and pending points. (only for Mobile Vikings) +- Monitor **Invoices**: Paid invoices, unpaid invoices, and the next expiration date. (only for Mobile Vikings) - Access subscription details, including: - - **Data Usage**: Remaining data and percentage used. + - **Data Usage**: Remaining data and percentage used. (only for Mobile Vikings) - **Voice Balance**: Minutes usage details. - - **SMS Balance**: SMS usage details. - - **Out of Bundle Costs**. + - **SMS Balance**: SMS usage details. (only for Mobile Vikings) + - **Out of Bundle Costs**. (only for Mobile Vikings) - **Credit Balance**. - **Subscription Product Information**. - **SIM Alias**. @@ -56,7 +60,7 @@ A Home Assistant integration to monitor Mobile Vikings BE services ## Table of Contents -- [Mobile Vikings for Home Assistant](#mobile-vikings-for-home-assistant) +- [Mobile Vikings and Jim Mobile for Home Assistant](#mobile-vikings--jimmobile-mobile-phone-subscription-information-for-home-assistant) - [Features](#features) - [Table of Contents](#table-of-contents) - [Installation](#installation) @@ -102,38 +106,39 @@ This integration will set up the following platforms. | `mobile_vikings` | Home Assistant component for Mobile Vikings BE services | ## Available Sensors +Support for Mobile Vikings (MV) or Jim Mobile (JM) ### Account Details -| Sensor Key | Description | Unit | -| -------------------------- | ------------------------ | -------- | -| `customer_info` | Customer's first name | Text | -| `loyalty_points_available` | Available loyalty points | € (Euro) | -| `loyalty_points_blocked` | Blocked loyalty points | € (Euro) | -| `loyalty_points_pending` | Pending loyalty points | € (Euro) | +| Sensor Key | Description | Unit | Support | +| -------------------------- | ------------------------ | -------- | ------- | +| `customer_info` | Customer's first name | Text | MV & JM | +| `loyalty_points_available` | Available loyalty points | € (Euro) | MV | +| `loyalty_points_blocked` | Blocked loyalty points | € (Euro) | MV | +| `loyalty_points_pending` | Pending loyalty points | € (Euro) | MV | ### Invoices -| Sensor Key | Description | Unit | -| ------------------------- | ---------------------------- | --------- | -| `paid_invoices` | Total paid invoices | Count | -| `unpaid_invoices` | Unpaid invoices total amount | € (Euro) | -| `next_invoice_expiration` | Next invoice expiration date | Timestamp | +| Sensor Key | Description | Unit | Support | +| ------------------------- | ---------------------------- | --------- | ------- | +| `paid_invoices` | Total paid invoices | Count | MV | +| `unpaid_invoices` | Unpaid invoices total amount | € (Euro) | MV | +| `next_invoice_expiration` | Next invoice expiration date | Timestamp | MV | ### Subscription Details -| Sensor Key | Description | Unit | -| -------------------- | ------------------------------ | ---------- | -| `data_balance` | Data usage percentage | % | -| `data_remaining` | Data remaining | GB | -| `remaining_days` | Days left in billing cycle | Days | -| `period_percentage` | Billing cycle usage percentage | % | -| `voice_balance` | Voice usage percentage | % | -| `sms_balance` | SMS usage percentage | % | -| `out_of_bundle_cost` | Out-of-bundle cost | € (Euro) | -| `credit` | Available credit balance | € (Euro) | -| `product_info` | Subscription product details | Text/Price | -| `sim_alias` | SIM alias | Text | +| Sensor Key | Description | Unit | Support | +| -------------------- | ------------------------------ | ---------- | ------- | +| `data_balance` | Data usage percentage | % | MV | +| `data_remaining` | Data remaining | GB | MV | +| `remaining_days` | Days left in billing cycle | Days | MV & JM | +| `period_percentage` | Billing cycle usage percentage | % | MV & JM | +| `voice_balance` | Voice usage percentage | % | MV & JM | +| `sms_balance` | SMS usage percentage | % | MV | +| `out_of_bundle_cost` | Out-of-bundle cost | € (Euro) | MV & JM | +| `credit` | Available credit balance | € (Euro) | MV & JM | +| `product_info` | Subscription product details | Text/Price | MV & JM | +| `sim_alias` | SIM alias | Text | MV & JM | --- @@ -161,8 +166,8 @@ Once you enable debug logging, you ideally need to make the error happen. Run yo ## Code origin -The code of this Home Assistant integration has been written initially by analysing the calls made by the Mobile Vikings website. +The code of this Home Assistant integration has been written initially by analysing the calls made by the Mobile Vikings and Jim Mobile website. The current version uses the MV Api as documented here: [https://docs.api.unleashed.be/](https://docs.api.unleashed.be/) -I have no link with Mobile Vikings +I have no link with Mobile Vikings or Jim Mobile diff --git a/custom_components/mobile_vikings/__init__.py b/custom_components/mobile_vikings/__init__.py index 4dac626..4a5d59e 100644 --- a/custom_components/mobile_vikings/__init__.py +++ b/custom_components/mobile_vikings/__init__.py @@ -1,4 +1,4 @@ -"""MobileVikings integration.""" +"""MobileVikings/JimMobile integration.""" from __future__ import annotations @@ -13,13 +13,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .client import MobileVikingsClient -from .const import COORDINATOR_UPDATE_INTERVAL, DOMAIN, PLATFORMS +from .const import COORDINATOR_UPDATE_INTERVAL, DOMAIN, PLATFORMS, MOBILE_VIKINGS _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up MobileVikings from a config entry.""" + """Set up MobileVikings / JimMobile from a config entry.""" hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} for platform in PLATFORMS: @@ -27,8 +27,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = MobileVikingsClient( hass=hass, + mobile_platform=entry.data.get('mobile_platform', MOBILE_VIKINGS), # Default to MOBILE_VIKINGS for backward compatibility username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + ) storage_dir = Path(f"{hass.config.path(STORAGE_DIR)}/{DOMAIN}") diff --git a/custom_components/mobile_vikings/__pycache__/client.cpython-312.pyc b/custom_components/mobile_vikings/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..c618d73 Binary files /dev/null and b/custom_components/mobile_vikings/__pycache__/client.cpython-312.pyc differ diff --git a/custom_components/mobile_vikings/binary_sensor.py b/custom_components/mobile_vikings/binary_sensor.py index 047d2be..06f44d3 100644 --- a/custom_components/mobile_vikings/binary_sensor.py +++ b/custom_components/mobile_vikings/binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify from . import MobileVikingsDataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, MOBILE_VIKINGS, JIM_MOBILE from .entity import MobileVikingsEntity from .utils import safe_get @@ -42,6 +42,7 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): subscription_types: tuple[str, ...] | None = ( None # Optional list of subscription types ) + mobile_platforms: tuple[str, ...] | None = None SUBSCRIPTION_SENSOR_TYPES: tuple[MobileVikingsBinarySensorDescription, ...] = ( @@ -53,10 +54,9 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_data_usage_alert" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("data", {}) - .get("usage_alert") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "data", "usage_alert"], default=None + ), value_fn=lambda data: safe_get( data, ["balance_aggregated", "data", "usage_alert"], default=0 ), @@ -72,6 +72,7 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): ), device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:alarm-light", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsBinarySensorDescription( key="subscriptions", @@ -81,9 +82,9 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_voice_usage_alert" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("voice", {}) - .get("usage_alert") + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "usage_alert"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "voice", "usage_alert"], default=0 @@ -100,6 +101,7 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): ), device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:alarm-light", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsBinarySensorDescription( key="subscriptions", @@ -109,10 +111,9 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_sms_usage_alert" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("sms", {}) - .get("usage_alert") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "sms", "usage_alert"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "sms", "usage_alert"], default=0 ), @@ -128,6 +129,7 @@ class MobileVikingsBinarySensorDescription(SensorEntityDescription): ), device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:alarm-light", + mobile_platforms=(MOBILE_VIKINGS), ), ) @@ -143,12 +145,17 @@ async def async_setup_entry( "coordinator" ] entities: list[MobileVikingsBinarySensor] = [] + mobile_platform = coordinator.client.mobile_platform + for subscription_id, subscription_data in coordinator.data.get( "subscriptions", [] ).items(): # Add static sensors from SUBSCRIPTION_SENSOR_TYPES for sensor_type in SUBSCRIPTION_SENSOR_TYPES: + if not sensor_type.mobile_platforms or mobile_platform not in sensor_type.mobile_platforms: + _LOGGER.debug(f"Skipping {sensor_type.key}-{sensor_type.translation_key} for mobile platform {mobile_platform}") + continue _LOGGER.debug( f"Searching for {sensor_type.key}-{sensor_type.translation_key}" ) diff --git a/custom_components/mobile_vikings/client.py b/custom_components/mobile_vikings/client.py index 51cfc60..61e16e0 100644 --- a/custom_components/mobile_vikings/client.py +++ b/custom_components/mobile_vikings/client.py @@ -5,7 +5,18 @@ from homeassistant.helpers.httpx_client import get_async_client -from .const import BASE_URL, CLIENT_ID, CLIENT_SECRET +from .const import ( + BASE_URL, + BASE_URL_JIMMOBILE, + CLIENT_ID, + CLIENT_ID_JIMMOBILE, + CLIENT_SECRET, + CLIENT_SECRET_TAG, + CLIENT_SECRET_TAG_JIMMOBILE, + CLIENT_SECRET_VALUE_JIMMOBILE, + MOBILE_VIKINGS, + JIM_MOBILE +) _LOGGER = logging.getLogger(__name__) @@ -19,7 +30,7 @@ class AuthenticationError(Exception): class MobileVikingsClient: """Asynchronous client for interacting with the Mobile Vikings API.""" - def __init__(self, hass, username, password, tokens=None): + def __init__(self, hass, username, password, mobile_platform, tokens=None): """Initialize the MobileVikingsClient. Parameters @@ -30,6 +41,8 @@ def __init__(self, hass, username, password, tokens=None): The username for authenticating with the Mobile Vikings API. password : str The password for authenticating with the Mobile Vikings API. + mobile_platform : str + The name of the mobile platform to connect to (Mobile Vikings or Jim Mobile). tokens : dict, optional A dictionary containing token information (refresh_token, access_token, expiry). @@ -37,6 +50,7 @@ def __init__(self, hass, username, password, tokens=None): self.hass = hass self.username = username self.password = password + self.mobile_platform = mobile_platform self.refresh_token = None self.access_token = None self.expires_in = None @@ -55,7 +69,10 @@ async def close(self): self.client = None async def authenticate(self): - """Authenticate with the Mobile Vikings API.""" + """Authenticate with the Mobile Vikings / JimMobile API.""" + actual_client_id = CLIENT_ID_JIMMOBILE if self.mobile_platform == JIM_MOBILE else CLIENT_ID + actual_client_secret_tag = CLIENT_SECRET_TAG_JIMMOBILE if self.mobile_platform == JIM_MOBILE else CLIENT_SECRET_TAG + actual_client_secret_value = CLIENT_SECRET_VALUE_JIMMOBILE if self.mobile_platform == JIM_MOBILE else CLIENT_SECRET if self._is_token_valid(): self.client.headers["Authorization"] = f"Bearer {self.access_token}" else: @@ -65,8 +82,8 @@ async def authenticate(self): { "refresh_token": self.refresh_token, "grant_type": "refresh_token", - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, + "client_id": actual_client_id, + actual_client_secret_tag: actual_client_secret_value, } ) else: @@ -76,8 +93,8 @@ async def authenticate(self): "username": self.username, "password": self.password, "grant_type": "password", - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, + "client_id": actual_client_id, + actual_client_secret_tag: actual_client_secret_value, } ) @@ -137,7 +154,10 @@ async def handle_request( if authenticate_request is False: await self.authenticate() - url = BASE_URL + endpoint + if self.mobile_platform == MOBILE_VIKINGS: + url = BASE_URL + endpoint + else: + url = BASE_URL_JIMMOBILE + endpoint request_details = f"{method} request to: {url}" # Anonymize sensitive information like passwords @@ -204,6 +224,9 @@ async def get_loyalty_points_balance(self): dict or None: A dictionary containing loyalty points balance, or None if request fails. """ + if self.mobile_platform == JIM_MOBILE: + # not existing for Jim Mobile + return {'error': 'not existing for Jim Mobile'} return await self.handle_request("/loyalty-points/balance") async def get_product_details(self, product_id): diff --git a/custom_components/mobile_vikings/config_flow.py b/custom_components/mobile_vikings/config_flow.py index 6f52e22..7922f26 100644 --- a/custom_components/mobile_vikings/config_flow.py +++ b/custom_components/mobile_vikings/config_flow.py @@ -13,12 +13,15 @@ TextSelector, TextSelectorConfig, TextSelectorType, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode ) from homeassistant.helpers.typing import UNDEFINED import voluptuous as vol from .client import MobileVikingsClient -from .const import DOMAIN, NAME +from .const import DOMAIN, NAME, MOBILE_VIKINGS, JIM_MOBILE from .exceptions import BadCredentialsException, MobileVikingsServiceException from .models import MobileVikingsConfigEntryData @@ -27,6 +30,7 @@ DEFAULT_ENTRY_DATA = MobileVikingsConfigEntryData( username=None, password=None, + mobile_platform=None ) @@ -54,6 +58,7 @@ async def async_validate_input(self, user_input: dict[str, Any]) -> None: hass=self.hass, username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], + mobile_platform=user_input['mobile_platform'] ) profile = await client.get_customer_info() @@ -72,12 +77,21 @@ async def async_step_connection_init( if not test["errors"]: self.new_title = user_input[CONF_USERNAME] self.new_entry_data |= user_input - await self.async_set_unique_id(f"{DOMAIN}_" + user_input[CONF_USERNAME]) + if user_input['mobile_platform'] == JIM_MOBILE: + await self.async_set_unique_id(f"{DOMAIN}_{JIM_MOBILE}_{user_input[CONF_USERNAME]}") + else: + await self.async_set_unique_id(f"{DOMAIN}_{user_input[CONF_USERNAME]}") self._abort_if_unique_id_configured() _LOGGER.debug(f"New account {self.new_title} added") return self.finish_flow() errors = test["errors"] fields = { + vol.Required('mobile_platform'): SelectSelector( + SelectSelectorConfig( + options=[MOBILE_VIKINGS, JIM_MOBILE], + mode=SelectSelectorMode.DROPDOWN + ) + ), vol.Required(CONF_USERNAME): TextSelector( TextSelectorConfig(type=TextSelectorType.EMAIL, autocomplete="username") ), diff --git a/custom_components/mobile_vikings/const.py b/custom_components/mobile_vikings/const.py index da87eba..d3589ad 100644 --- a/custom_components/mobile_vikings/const.py +++ b/custom_components/mobile_vikings/const.py @@ -17,19 +17,32 @@ # Date and time format used in the Mobile Vikings API responses DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z" -# Client ID for OAuth2 authentication +MOBILE_VIKINGS = "Mobile Vikings" +JIM_MOBILE = "Jim Mobile" + +# Client ID for OAuth2 authentication FPID2.2.MCaIbSP%2FL5s3sb2U72khFTgLJ68VabpnVuB6HzKd494%3D.1737890525 CLIENT_ID = "orIbwDyPep4Cju3CksWC4AXiIezY6JVHDix8I9pL" # Client secret for OAuth2 authentication CLIENT_SECRET = "QquFECtjqYev6DgApjrBsOzTPFwCEv8DTBQOBMSNs77YtwIzxPGagNhDpwwt8wxOwP8B4nd4gCTvVZfuWccTfKCPSh1xVruqHjrFBs1fH4Y8lSSRcw7PPL1QlcZwAY24" +CLIENT_SECRET_TAG = "client_secret" + +CLIENT_SECRET_TAG_JIMMOBILE = "grant_type" +CLIENT_SECRET_VALUE_JIMMOBILE = "password" +CLIENT_ID_JIMMOBILE = "JIM" + # Base URL for the Mobile Vikings API BASE_URL = "https://uwa.mobilevikings.be/latest/mv" +# Base URL for the JimMobile API +BASE_URL_JIMMOBILE = "https://uwa.mobilevikings.be/jim" + COORDINATOR_UPDATE_INTERVAL = timedelta(minutes=15) CONNECTION_RETRY = 5 REQUEST_TIMEOUT = 20 WEBSITE = "https://mobilevikings.be/nl/my-viking" +WEBSITE_JIMMOBILE = "https://jimmobile.be/nl/" DEFAULT_ICON = "mdi:help-circle-outline" diff --git a/custom_components/mobile_vikings/entity.py b/custom_components/mobile_vikings/entity.py index 7f10561..7dbb7ab 100644 --- a/custom_components/mobile_vikings/entity.py +++ b/custom_components/mobile_vikings/entity.py @@ -12,7 +12,7 @@ from homeassistant.util import slugify from . import MobileVikingsDataUpdateCoordinator -from .const import ATTRIBUTION, DOMAIN, NAME, VERSION, WEBSITE +from .const import ATTRIBUTION, DOMAIN, NAME, VERSION, WEBSITE, WEBSITE_JIMMOBILE, MOBILE_VIKINGS, JIM_MOBILE _LOGGER = logging.getLogger(__name__) @@ -38,6 +38,7 @@ def __init__( super().__init__(coordinator) self.idx = idx self.entity_description = description + self.mobile_platform = coordinator.client.mobile_platform self._identifier = f"{description.key}" self._attr_device_info = DeviceInfo( identifiers={ @@ -48,8 +49,8 @@ def __init__( }, name=self.entity_description.device_name_fn(self.item), translation_key=slugify(self.entity_description.device_name_fn(self.item)), - manufacturer=NAME, - configuration_url=WEBSITE, + manufacturer=NAME if self.mobile_platform == MOBILE_VIKINGS else JIM_MOBILE, + configuration_url=WEBSITE if self.mobile_platform == MOBILE_VIKINGS else WEBSITE_JIMMOBILE, entry_type=DeviceEntryType.SERVICE, model=self.entity_description.model_fn(self.item), sw_version=VERSION, diff --git a/custom_components/mobile_vikings/manifest.json b/custom_components/mobile_vikings/manifest.json index 5665b3d..c408e67 100644 --- a/custom_components/mobile_vikings/manifest.json +++ b/custom_components/mobile_vikings/manifest.json @@ -11,5 +11,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/geertmeersman/mobile_vikings/issues", "requirements": [], - "version": "v1.0.6" + "version": "v2.0.0" } diff --git a/custom_components/mobile_vikings/models.py b/custom_components/mobile_vikings/models.py index 139d2d0..064e6bb 100644 --- a/custom_components/mobile_vikings/models.py +++ b/custom_components/mobile_vikings/models.py @@ -11,6 +11,7 @@ class MobileVikingsConfigEntryData(TypedDict): username: str | None password: str | None + mobile_platform: str | None @dataclass diff --git a/custom_components/mobile_vikings/sensor.py b/custom_components/mobile_vikings/sensor.py index 4dadcae..a57409b 100644 --- a/custom_components/mobile_vikings/sensor.py +++ b/custom_components/mobile_vikings/sensor.py @@ -1,4 +1,4 @@ -"""Mobile Vikings sensor platform.""" +"""Mobile Vikings / Jim Mobile sensor platform.""" from __future__ import annotations @@ -22,7 +22,7 @@ from homeassistant.util import slugify from . import MobileVikingsDataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, MOBILE_VIKINGS, JIM_MOBILE from .entity import MobileVikingsEntity from .utils import safe_get, to_title_case_with_spaces @@ -45,6 +45,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): subscription_types: tuple[str, ...] | None = ( None # Optional list of subscription types ) + mobile_platforms: tuple[str, ...] | None = None SENSOR_TYPES: tuple[MobileVikingsSensorDescription, ...] = ( @@ -59,66 +60,71 @@ class MobileVikingsSensorDescription(SensorEntityDescription): device_identifier_fn=lambda data: "Customer", model_fn=lambda data: "Customer Info", attributes_fn=lambda data: data, + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), ), MobileVikingsSensorDescription( key="loyalty_points_balance", translation_key="loyalty_points_available", unique_id_fn=lambda data: "loyalty_points_available", icon="mdi:currency-eur", - available_fn=lambda data: data.get("available") is not None, - value_fn=lambda data: data.get("available"), + available_fn=lambda data: data is not None and data.get("available") is not None, + value_fn=lambda data: 0 if data is None else data.get("available", 0), device_name_fn=lambda data: "Loyalty Points", device_identifier_fn=lambda data: "Loyalty Points", model_fn=lambda data: "Loyalty Points", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="loyalty_points_balance", translation_key="loyalty_points_blocked", unique_id_fn=lambda data: "loyalty_points_blocked", icon="mdi:currency-eur-off", - available_fn=lambda data: data.get("blocked") is not None, - value_fn=lambda data: data.get("blocked"), + available_fn=lambda data: data is not None and data.get("blocked") is not None, + value_fn=lambda data: 0 if data is None else data.get("blocked", 0), device_name_fn=lambda data: "Loyalty Points", device_identifier_fn=lambda data: "Loyalty Points", model_fn=lambda data: "Loyalty Points", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="loyalty_points_balance", translation_key="loyalty_points_pending", unique_id_fn=lambda data: "loyalty_points_pending", icon="mdi:timer-sand", - available_fn=lambda data: data.get("pending") is not None, - value_fn=lambda data: data.get("pending"), + available_fn=lambda data: data is not None and data.get("pending") is not None, + value_fn=lambda data: 0 if data is None else data.get("pending", 0), device_name_fn=lambda data: "Loyalty Points", device_identifier_fn=lambda data: "Loyalty Points", model_fn=lambda data: "Loyalty Points", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="paid_invoices", translation_key="paid_invoices", unique_id_fn=lambda data: "paid_invoices", icon="mdi:receipt-text-check", - available_fn=lambda data: data.get("results") is not None, - value_fn=lambda data: data.get("total_items"), + available_fn=lambda data: data is not None and data.get("results") is not None, + value_fn=lambda data: 0 if data is None else data.get("total_items", 0), device_name_fn=lambda data: "Invoices", device_identifier_fn=lambda data: "Invoices", model_fn=lambda data: "Invoices", attributes_fn=lambda data: { "invoices": safe_get(data, ["results"], default=[]) }, + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="unpaid_invoices", translation_key="unpaid_invoices", unique_id_fn=lambda data: "unpaid_invoices", icon="mdi:currency-eur", - available_fn=lambda data: data.get("results") is not None, + available_fn=lambda data: data is not None and data.get("results") is not None, value_fn=lambda data: sum( item.get("amount_due", 0) for item in data.get("results", []) ), @@ -130,13 +136,14 @@ class MobileVikingsSensorDescription(SensorEntityDescription): }, device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="unpaid_invoices", translation_key="next_invoice_expiration", unique_id_fn=lambda data: "next_invoice_expiration", icon="mdi:calendar-star", - available_fn=lambda data: data.get("results") is not None, + available_fn=lambda data: data is not None and data.get("results") is not None, value_fn=lambda data: min( ( datetime.fromisoformat(item["expiration_date"].replace("Z", "+00:00")) @@ -169,6 +176,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): "results": safe_get(data, ["results"], default={}), }, device_class=SensorDeviceClass.TIMESTAMP, + mobile_platforms=(MOBILE_VIKINGS), ), ) @@ -182,10 +190,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_data_balance" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("data", {}) - .get("used_percentage") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "data", "used_percentage"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "data", "used_percentage"], default=0 ), @@ -202,6 +209,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, suggested_display_precision=0, icon="mdi:signal-4g", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="subscriptions", @@ -211,10 +219,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_data_remaining" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("data", {}) - .get("remaining_gb") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "data", "remaining_gb"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "data", "remaining_gb"], default=0 ), @@ -230,6 +237,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): ), native_unit_of_measurement=UnitOfInformation.GIGABYTES, icon="mdi:signal-4g", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="subscriptions", @@ -239,10 +247,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_remaining_days" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("data", {}) - .get("remaining_days") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "data", "remaining_days"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "data", "remaining_days"], default=0 ), @@ -255,6 +262,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): ), native_unit_of_measurement=UnitOfTime.DAYS, icon="mdi:calendar-end-outline", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="subscriptions", @@ -264,10 +272,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_period_pct" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("data", {}) - .get("period_percentage") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "data", "period_percentage"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "data", "period_percentage"], default=0 ), @@ -281,6 +288,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, suggested_display_precision=0, icon="mdi:calendar-clock", + mobile_platforms=(MOBILE_VIKINGS), ), # Voice balance MobileVikingsSensorDescription( @@ -291,10 +299,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_voice_balance" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("voice", {}) - .get("used_percentage") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "used_percentage"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "voice", "used_percentage"], default=0 ), @@ -311,6 +318,58 @@ class MobileVikingsSensorDescription(SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, suggested_display_precision=0, icon="mdi:phone", + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), + ), + MobileVikingsSensorDescription( + key="subscriptions", + subscription_types=("prepaid"), + translation_key="period_percentage", + unique_id_fn=lambda data: ( + (data.get("sim") or {}).get("msisdn", "") + "_voice_period_pct" + ), + entity_id_prefix_fn=lambda data: "", + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "period_percentage"], default=None + ) is not None, + value_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "period_percentage"], default=0 + ), + device_name_fn=lambda data: "Subscription", + device_identifier_fn=lambda data: "Subscription " + data.get("id", ""), + model_fn=lambda data: (data.get("sim") or {}).get("msisdn", "") + + " - " + + safe_get( + data, ["product", "descriptions", "title"], default="Unknown Product" + ), + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + icon="mdi:calendar-clock", + mobile_platforms=(JIM_MOBILE), + ), + MobileVikingsSensorDescription( + key="subscriptions", + subscription_types=("postpaid", "prepaid", "data-only"), + translation_key="remaining_days", + unique_id_fn=lambda data: ( + (data.get("sim") or {}).get("msisdn", "") + "_voice_remaining_days" + ), + entity_id_prefix_fn=lambda data: "", + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "remaining_days"], default=None + ) is not None, + value_fn=lambda data: safe_get( + data, ["balance_aggregated", "voice", "remaining_days"], default=0 + ), + device_name_fn=lambda data: "Subscription", + device_identifier_fn=lambda data: "Subscription " + data.get("id", ""), + model_fn=lambda data: (data.get("sim") or {}).get("msisdn", "") + + " - " + + safe_get( + data, ["product", "descriptions", "title"], default="Unknown Product" + ), + native_unit_of_measurement=UnitOfTime.DAYS, + icon="mdi:calendar-end-outline", + mobile_platforms=(JIM_MOBILE), ), # SMS balance MobileVikingsSensorDescription( @@ -321,10 +380,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_sms_balance" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance_aggregated", {}) - .get("sms", {}) - .get("used_percentage") - is not None, + available_fn=lambda data: safe_get( + data, ["balance_aggregated", "sms", "used_percentage"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance_aggregated", "sms", "used_percentage"], default=0 ), @@ -341,6 +399,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, suggested_display_precision=0, icon="mdi:message", + mobile_platforms=(MOBILE_VIKINGS), ), MobileVikingsSensorDescription( key="subscriptions", @@ -350,8 +409,9 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_out_of_bundle_cost" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance", {}).get("out_of_bundle_cost") - is not None, + available_fn=lambda data: safe_get( + data, ["balance", "out_of_bundle_cost"], default=None + ) is not None, value_fn=lambda data: safe_get( data, ["balance", "out_of_bundle_cost"], default=0 ), @@ -365,6 +425,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:currency-eur", + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), ), MobileVikingsSensorDescription( key="subscriptions", @@ -374,7 +435,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_credit" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("balance", {}).get("credit") is not None, + available_fn=lambda data: safe_get(data, ["balance", "credit"], default=None) is not None, value_fn=lambda data: safe_get(data, ["balance", "credit"], default=0), device_name_fn=lambda data: "Subscription", device_identifier_fn=lambda data: "Subscription " + data.get("id", ""), @@ -386,6 +447,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:currency-eur", + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), ), MobileVikingsSensorDescription( key="subscriptions", @@ -395,7 +457,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_product_info" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("product", {}).get("price") is not None, + available_fn=lambda data: safe_get(data, ["product", "price"], default=None) is not None, value_fn=lambda data: safe_get(data, ["product", "price"], default=0.0), device_name_fn=lambda data: "Subscription", device_identifier_fn=lambda data: "Subscription " + data.get("id", ""), @@ -408,6 +470,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:package-variant", + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), ), MobileVikingsSensorDescription( key="subscriptions", @@ -417,7 +480,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): (data.get("sim") or {}).get("msisdn", "") + "_sim_alias" ), entity_id_prefix_fn=lambda data: "", - available_fn=lambda data: data.get("sim", {}).get("alias") is not None, + available_fn=lambda data: safe_get(data, ["sim", "alias"], default=None) is not None, value_fn=lambda data: safe_get(data, ["sim", "alias"], default=""), device_name_fn=lambda data: "Subscription", device_identifier_fn=lambda data: "Subscription " + data.get("id", ""), @@ -428,6 +491,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): ), attributes_fn=lambda data: safe_get(data, ["sim"], default={}), icon="mdi:sim", + mobile_platforms=(MOBILE_VIKINGS, JIM_MOBILE), ), MobileVikingsSensorDescription( key="subscriptions", @@ -452,6 +516,7 @@ class MobileVikingsSensorDescription(SensorEntityDescription): ), attributes_fn=lambda data: safe_get(data, ["modem_settings"], default={}), icon="mdi:router-network-wireless", + mobile_platforms=(MOBILE_VIKINGS), ), ) @@ -468,9 +533,13 @@ async def async_setup_entry( ] entities: list[MobileVikingsSensor] = [] + mobile_platform = coordinator.client.mobile_platform # Add static sensors from SENSOR_TYPES for sensor_type in SENSOR_TYPES: + if mobile_platform not in sensor_type.mobile_platforms: + _LOGGER.debug(f"Skipping {sensor_type.key}-{sensor_type.translation_key} for mobile platform {mobile_platform}") + continue _LOGGER.debug(f"Searching for {sensor_type.key}-{sensor_type.translation_key}") if sensor_type.key in coordinator.data: entities.append(MobileVikingsSensor(coordinator, sensor_type, entry, None)) @@ -480,6 +549,9 @@ async def async_setup_entry( ).items(): # Add static sensors from SUBSCRIPTION_SENSOR_TYPES for sensor_type in SUBSCRIPTION_SENSOR_TYPES: + if mobile_platform not in sensor_type.mobile_platforms: + _LOGGER.debug(f"Skipping {sensor_type.key}-{sensor_type.translation_key} for mobile platform {mobile_platform}") + continue _LOGGER.debug( f"Searching for {sensor_type.key}-{sensor_type.translation_key}" ) diff --git a/custom_components/mobile_vikings/translations/en.json b/custom_components/mobile_vikings/translations/en.json index 6d01b75..769050f 100644 --- a/custom_components/mobile_vikings/translations/en.json +++ b/custom_components/mobile_vikings/translations/en.json @@ -12,11 +12,12 @@ "step": { "connection_init": { "data": { + "mobile_platform": "Mobile platform", "password": "Password", "username": "Username" }, - "description": "Set up your Mobile Vikings user account", - "title": "New Mobile Vikings account" + "description": "Set up your Mobile Vikings or Jim Mobile user account", + "title": "New Mobile Vikings or Jim Mobile account" } } }, @@ -89,6 +90,9 @@ "product_info": { "name": "Product info" }, + "bundles_info": { + "name": "Prepaid bundles info" + }, "remaining_days": { "name": "Remaining days" }, @@ -127,7 +131,7 @@ "data": { "password": "Password" }, - "description": "To do when you changed your password on 'Mobile Vikings'", + "description": "To use when you changed your password on 'Mobile Vikings' or 'Jim Mobile'", "title": "Update your password" } } diff --git a/custom_components/mobile_vikings/translations/fr.json b/custom_components/mobile_vikings/translations/fr.json index 724f7a3..ff571ca 100644 --- a/custom_components/mobile_vikings/translations/fr.json +++ b/custom_components/mobile_vikings/translations/fr.json @@ -12,11 +12,12 @@ "step": { "connection_init": { "data": { + "mobile_platform": "Platform mobile", "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Configurez votre compte utilisateur Mobile Vikings", - "title": "Nouveau compte Mobile Vikings" + "description": "Configurez votre compte utilisateur Mobile Vikings ou Jim Mobile", + "title": "Nouveau compte Mobile Vikings ou Jim Mobile" } } }, @@ -89,6 +90,9 @@ "product_info": { "name": "Infos produit" }, + "bundles_info": { + "name": "Infos bundle prepaid" + }, "remaining_days": { "name": "Jours restants" }, @@ -127,7 +131,7 @@ "data": { "password": "Mot de passe" }, - "description": "\u00c0 faire lorsque vous avez modifi\u00e9 votre mot de passe sur 'Mobile Vikings'", + "description": "\u00c0 faire lorsque vous avez modifi\u00e9 votre mot de passe sur 'Mobile Vikings' ou 'Jim Mobile'", "title": "Mettre \u00e0 jour votre mot de passe" } } diff --git a/custom_components/mobile_vikings/translations/nl.json b/custom_components/mobile_vikings/translations/nl.json index 95e4ae8..6aa442f 100644 --- a/custom_components/mobile_vikings/translations/nl.json +++ b/custom_components/mobile_vikings/translations/nl.json @@ -12,11 +12,12 @@ "step": { "connection_init": { "data": { + "mobile_platform": "Mobiel platform", "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Stel uw Mobile Vikings-gebruikersaccount in", - "title": "Nieuw Mobile Vikings-account" + "description": "Stel uw Mobile Vikings of JimMobile gebruikersaccount in", + "title": "Nieuw Mobile Vikings of JimMobile account" } } }, @@ -89,6 +90,9 @@ "product_info": { "name": "Productinformatie" }, + "bundles_info": { + "name": "Prepaid bundel informatie" + }, "remaining_days": { "name": "Resterende dagen" }, @@ -127,7 +131,7 @@ "data": { "password": "Wachtwoord" }, - "description": "Te doen wanneer u uw wachtwoord op 'Mobile Vikings' heeft gewijzigd", + "description": "Te gebruiken wanneer u uw wachtwoord op 'Mobile Vikings' of 'Jim Mobile' heeft gewijzigd", "title": "Werk uw wachtwoord bij" } } diff --git a/images/brand/jimmobile.png b/images/brand/jimmobile.png new file mode 100644 index 0000000..e10a735 Binary files /dev/null and b/images/brand/jimmobile.png differ