Metadata-Version: 2.4
Name: hidroconta
Version: 3.1.2
Summary: DMeter360 REST API Wrapper for Python, provided by Hidroconta S.A.U.
Author: JavierL
Author-email: javier.lopez@hidroconta.com
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: pandas>=1.3.5
Requires-Dist: requests>=2.26.0
Requires-Dist: datetime>=5.5
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# HIDROCONTA - DMeter360 REST API Python Wrapper

A Python library that makes it easy to interact with Hidroconta's **DMeter360 REST API**. It wraps all API calls into a clean, object-oriented interface and integrates with **pandas** for efficient large-scale data handling.

- **PyPI:** https://pypi.org/project/hidroconta/
- **Current version:** 3.1.1 (23/06/2026)
- **Contact:** javier.lopez@hidroconta.com

> **Note:** Version 3.0.0 is **not backwards-compatible** with v1.x or v2.x.

---

## Table of Contents

1. [Installation](#installation)
2. [Quick Start](#quick-start)
3. [Authentication](#authentication)
   - [API Key Login (Recommended)](#api-key-login-recommended)
   - [Username & Password Login](#username--password-login)
   - [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
4. [Available Servers](#available-servers)
5. [Response Format: JSON vs Pandas](#response-format-json-vs-pandas)
6. [Reading Data](#reading-data)
   - [Search](#search)
   - [Get Elements](#get-elements)
   - [Get Historical Data](#get-historical-data)
   - [Get Minute Consumption](#get-minute-consumption)
7. [Writing Data](#writing-data)
   - [Update Counter Global Value](#update-counter-global-value)
   - [Update Analog Input Value](#update-analog-input-value)
   - [Update Nautilus+ Values](#update-nautilus-values)
   - [Add Historical Records](#add-historical-records)
8. [Managing Elements](#managing-elements)
   - [Create Water Meter Counter](#create-water-meter-counter)
   - [Delete Element](#delete-element)
9. [User & Permission Management](#user--permission-management)
   - [Create User](#create-user)
   - [Create Users from CSV](#create-users-from-csv)
   - [Grant Permissions](#grant-permissions)
10. [API Key Management](#api-key-management)
11. [Types Reference](#types-reference)
    - [Element Types](#element-types)
    - [Historical Data Types](#historical-data-types)
    - [Status](#status)
    - [Roles](#roles)
12. [Error Handling](#error-handling)
13. [Utilities](#utilities)
14. [Full Working Example](#full-working-example)
15. [Version History](#version-history)

---

## Installation

Make sure you have **Python 3.9 or higher** installed.

```bash
pip install hidroconta
```

**Dependencies** (installed automatically):
- `pandas >= 1.3.5` â€” for DataFrame support
- `requests >= 2.26.0` â€” for HTTP calls

---

## Quick Start

This is the minimal code needed to connect and retrieve data:

```python
import hidroconta.api as demeter
import hidroconta.endpoints as endpoints

# Connect to the main server using an API key
with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    client.login(api_key='YOUR_API_KEY')

    # Search for elements matching 'SAT'
    result = client.search(text='SAT', pandas=True)
    print(result)
```

The `with` statement ensures the session is properly closed (logout + cleanup) when the block ends, even if an error occurs.

---

## Authentication

### API Key Login (Recommended)

The simplest and most secure way to authenticate. API keys can be generated from within the library (see [API Key Management](#api-key-management)).

```python
with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    client.login(api_key='YOUR_API_KEY_HERE')
```

### Username & Password Login

```python
with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    client.login(username='your_username', password='your_password')
```

### Multi-Factor Authentication (MFA)

If MFA is enabled on your account, logging in will raise a `MfaRequiredException`. You must then validate the MFA code to complete login.

**Email OTP:**

```python
import hidroconta.api as demeter
import hidroconta.endpoints as endpoints

with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    try:
        client.login(username='your_username', password='your_password')
    except demeter.MfaRequiredException as mfa:
        # mfa.mfa_token contains the token needed for validation
        client.validate_mfa_email(
            code='123456',           # The 6-digit code sent to your email
            mfa_token=mfa.mfa_token,
            device_nickname='My PC'  # A friendly name for this device
        )
```

**TOTP (Authenticator App):**

```python
    except demeter.MfaRequiredException as mfa:
        client.validate_mfa_totp(
            code='123456',           # Code from your authenticator app
            mfa_token=mfa.mfa_token,
            device_nickname='My PC'
        )
```

---

## Available Servers

Choose the server that corresponds to your DMeter360 deployment:

| Server Name | URL |
|---|---|
| `Server.MAIN` | `https://demeter2.hidroconta.com/Demeter2/v2/` |
| `Server.GESTAGUA` | `https://demeter-gestagua.hidroconta.com/Demeter2/v2/` |
| `Server.TUDELA` | `https://demeter-tudela-de-duero.hidroconta.com/Demeter2/v2/` |
| `Server.AIC` | `https://demeter-aic.hidroconta.com/Demeter2/v2/` |
| `Server.UTEBO` | `https://demeter-utebo.hidroconta.com/Demeter2/v2/` |
| `Server.TEST` | `https://demeter2-test-ovh.hidroconta.com/Demeter2/v2/` |

```python
import hidroconta.endpoints as endpoints

# Example: connect to GESTAGUA server
with demeter.DemeterClient(endpoints.Server.GESTAGUA) as client:
    client.login(api_key='YOUR_API_KEY')
```

---

## Response Format: JSON vs Pandas

All data-retrieval methods support two return formats:

| Parameter | Return type | When to use |
|---|---|---|
| `pandas=False` *(default)* | Raw JSON string | Direct API responses, custom parsing |
| `pandas=True` | `pandas.DataFrame` | Data analysis, large datasets, CSV export |

```python
# Returns raw JSON string
json_data = client.get_rtus()

# Returns a pandas DataFrame
df = client.get_rtus(pandas=True)
print(df.head())
```

---

## Reading Data

### Search

Search across multiple element types by text. This is the fastest way to find an element when you know part of its name or code.

```python
import hidroconta.types as tp

df = client.search(
    text='SAT-001',                            # Text to search (name, code, etc.)
    element_types=[tp.Element.COUNTER,          # Which element types to include
                   tp.Element.RTU,
                   tp.Element.ANALOG_INPUT],
    status=tp.Status.ENABLED,                  # Filter by status (ENABLED, DISABLED, ALL)
    pandas=True
)
print(df)
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `text` | `str` | Yes | â€” | Search text |
| `element_types` | `list[Element]` | No | All types | Element types to search |
| `status` | `Status` | No | `Status.ALL` | Filter by enabled/disabled |
| `pandas` | `bool` | No | `False` | Return DataFrame instead of JSON |

---

### Get Elements

Retrieve all elements of a given type, or a single element by its ID.

Each device type has its own method. All follow the same pattern: call with no arguments to get all, or pass `element_id` to get one.

```python
# Get all RTUs
all_rtus = client.get_rtus(pandas=True)

# Get a specific RTU by its ID
one_rtu = client.get_rtus(element_id=17512, pandas=True)

# Get all counters
counters = client.get_counters(pandas=True)

# Get all analog inputs
analog_inputs = client.get_analog_inputs(pandas=True)

# Get all digital inputs
digital_inputs = client.get_digital_inputs(pandas=True)

# IRIS devices (IoT connectivity)
iris_nb     = client.get_iris_nb(pandas=True)       # NB-IoT
iris_lw     = client.get_iris_lw(pandas=True)       # LoRaWAN
iris_sigfox = client.get_iris_sigfox(pandas=True)   # Sigfox
iris_gprs   = client.get_iris_gprs(pandas=True)     # GPRS

# Other device types
installations = client.get_installations(pandas=True)
centinel      = client.get_centinel(pandas=True)
centaurus     = client.get_centaurus(pandas=True)
nautilus      = client.get_nautilus(pandas=True)     # Nautilus Plus
```

**Available get methods summary:**

| Method | Device Type |
|---|---|
| `get_rtus()` | RTU data loggers |
| `get_counters()` | Water meter counters |
| `get_analog_inputs()` | Analog input sensors |
| `get_digital_inputs()` | Digital input sensors |
| `get_iris_nb()` | IRIS NB-IoT devices |
| `get_iris_lw()` | IRIS LoRaWAN devices |
| `get_iris_sigfox()` | IRIS Sigfox devices |
| `get_iris_gprs()` | IRIS GPRS devices |
| `get_installations()` | Installations |
| `get_centinel()` | Centinel devices |
| `get_centaurus()` | Centaurus devices |
| `get_nautilus()` | Nautilus Plus meters |

---

### Get Historical Data

Retrieve time-series historical readings for one or more elements. You must specify the type of data using `subtype` and `subcode` from `hidroconta.types`.

```python
import datetime
import hidroconta.types as tp

# Get the last 7 days of counter readings
end   = datetime.datetime.now()
start = end - datetime.timedelta(days=7)

df = client.get_historics(
    start_date=start,
    end_date=end,
    element_ids=[1001, 1002, 1003],         # List of element IDs
    subtype=tp.CounterGlobalHist.subtype,   # Data type category
    subcode=[tp.CounterGlobalHist.subcode], # Data subtype (list)
    pandas=True
)
print(df)
```

**Parameters:**

| Parameter | Type | Required | Description |
|---|---|---|---|
| `start_date` | `datetime` | Yes | Start of the time range |
| `end_date` | `datetime` | Yes | End of the time range |
| `element_ids` | `list[int]` | Yes | List of element IDs to query |
| `subtype` | `int` | Yes | Historical data type (`HistType.subtype`) |
| `subcode` | `list[int]` | Yes | Historical data subcode (`[HistType.subcode]`) |
| `pandas` | `bool` | No | Return DataFrame instead of JSON |

**Common historical data types:**

```python
# Counter total volume
subtype=tp.CounterGlobalHist.subtype, subcode=[tp.CounterGlobalHist.subcode]

# Flow rate
subtype=tp.FlowHist.subtype, subcode=[tp.FlowHist.subcode]

# Analog input (pressure, temperature, etc.)
subtype=tp.AnalogInputHist.subtype, subcode=[tp.AnalogInputHist.subcode]

# IRIS counter total volume
subtype=tp.IrisCounterGlobalHist.subtype, subcode=[tp.IrisCounterGlobalHist.subcode]

# Nautilus Plus - positive volume
subtype=tp.NautilusPlusPositiveCounterGlobalHist.subtype, subcode=[tp.NautilusPlusPositiveCounterGlobalHist.subcode]

# Custom type (if you know the exact subtype/subcode values)
subtype=tp.CustomHist(subtype=99, subcode=1).subtype, subcode=[tp.CustomHist(subtype=99, subcode=1).subcode]
```

---

### Get Minute Consumption

Retrieve consumption data aggregated by minute intervals (for flow/volume analysis).

```python
import datetime

end   = datetime.datetime.now()
start = end - datetime.timedelta(hours=24)

df = client.get_minute_consumption(
    start_date=start,
    end_date=end,
    element_ids=[1001],
    period_value=15,    # Aggregation period in minutes
    min_interval=1,     # Minimum data interval
    pandas=True
)
print(df)
```

---

## Writing Data

### Update Counter Global Value

Update the total accumulated volume for a counter. Used to synchronize a physical meter reading with the system.

```python
import datetime

client.global_update(
    rtu_id=12345,                          # ID of the RTU the counter is attached to
    timestamp=datetime.datetime.now(),     # When the reading was taken
    liters=1500000,                        # Total accumulated volume in liters
    position=0,                            # Counter position (0-3)
    expansion=0                            # Expansion board index (usually 0)
)
```

**Parameters:**

| Parameter | Type | Description |
|---|---|---|
| `rtu_id` | `int` | RTU identifier |
| `timestamp` | `datetime` | Timestamp of the reading |
| `liters` | `int` | Total accumulated volume in liters |
| `position` | `int` | Counter position on the RTU (0â€“3) |
| `expansion` | `int` | Expansion board index (0 = main board) |

---

### Update Analog Input Value

Update the current value of an analog sensor (e.g., a pressure or level sensor).

```python
import datetime

client.update_analog_input_value(
    rtu_id=12345,
    position=1,                            # Analog input channel (0-based)
    expansion=0,
    value=4.75,                            # Sensor reading
    timestamp=datetime.datetime.now()
)
```

---

### Update Nautilus+ Values

Update all measurement values for a Nautilus Plus smart water meter in a single call.

```python
import datetime

client.update_nautilus_values(
    rtu_id=863266050913899,                    # RTU ID for the Nautilus+ device
    positive_value_liters=2000,               # Positive (forward) counter total
    positive_flow_volume_militers=100,        # Positive flow volume (milliliters)
    negative_value_liters=800,                # Negative (reverse) counter total
    negative_flow_volume_militers=50,
    net_value_liters=1200,                    # Net counter (positive - negative)
    net_flow_volume_militers=50,
    flow_rate_liters_per_second=12,           # Current flow rate
    water_temperature_celsius=16.0,           # Water temperature
    pressure_bars=3.5,                        # Water pressure
    timestamp=datetime.datetime.now()         # Optional, defaults to now
)
```

---

### Add Historical Records

Manually push one or more historical data records for an RTU. Use the `Hist` class to build each record.

```python
import datetime
import hidroconta.hist as hist
import hidroconta.types as tp

# Build a historical record
record = hist.Hist(
    timestamp=datetime.datetime(2024, 6, 1, 12, 0, 0),
    value=1000,                   # Reading value
    position=0,                   # Counter position
    expansion=0,
    type=tp.CounterGlobalHist     # Type of data
)

client.add_historics(
    rtu_id=12345,
    historics=[record]            # List of Hist objects
)
```

---

## Managing Elements

### Create Water Meter Counter

Create a new water meter counter on an existing RTU. Returns a DataFrame with the newly created counter details.

By default, `position` and `expansion` are assigned automatically based on the existing counters on the RTU. You can override them explicitly if needed.

```python
# Auto-assign position and expansion (recommended)
df = client.create_demeter_watermeter(
    rtu_id=12345,
    code='METER_ZONE_A_01',
    serial_number='SN-00123456'
)
print(df)

# Manually specify position and expansion
df = client.create_demeter_watermeter(
    rtu_id=12345,
    code='METER_ZONE_A_02',
    serial_number='SN-00123457',
    position=2,
    expansion=0
)
print(df)
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `rtu_id` | `int` | Yes | â€” | RTU to attach the counter to |
| `code` | `str` | Yes | â€” | Unique code for the new counter |
| `serial_number` | `str` | Yes | â€” | Physical serial number of the water meter |
| `position` | `int` | No | `-1` (auto) | Counter position on the RTU (0â€“3). Auto-detected if `-1` |
| `expansion` | `int` | No | `-1` (auto) | Expansion board index. Auto-detected if `-1` |

---

### Delete Element

Delete an element by its ID and type. A confirmation prompt is shown by default.

```python
import hidroconta.types as tp

# Delete a counter with confirmation prompt
client.delete_element(
    element_id=9999,
    type=tp.Element.COUNTER
)

# Delete without confirmation (use with caution)
client.delete_element(
    element_id=9999,
    type=tp.Element.COUNTER,
    confirmation=True
)
```

> **Warning:** Deletion is permanent. Always verify the element ID before using `confirmation=True`.

---

## User & Permission Management

### Create User

Create a single new user in the system.

```python
import hidroconta.types as tp

client.create_user(
    username='john.doe',
    role=tp.Role.BASIC,                       # User role
    installations=[101, 102],                 # Installations the user can access
    description='Field technician zone A',
    access=tp.Access.COMMON                   # Access level
)
```

**Available roles:**

| Role | Description |
|---|---|
| `Role.BASIC` | Read-only access to assigned elements |
| `Role.INSTALLER` | Can manage devices in assigned installations |
| `Role.MANUFACTURING` | Manufacturing/setup operations |
| `Role.ADMIN` | Full administrative access |
| `Role.ADVANCED` | Advanced operations |
| `Role.HIDROCONTA` | Hidroconta internal role |

**Access levels:**

| Access | Description |
|---|---|
| `Access.COMMON` | Default standard access |
| `Access.ALL_READ` | Read access to all elements |
| `Access.ALL_WRITE` | Write access to all elements |

---

### Create Users from CSV

Create multiple users at once from a CSV file and automatically grant them permissions. This is useful for onboarding many users in bulk.

```python
client.create_user_with_permission(
    file_route='C:/users/new_users.csv'
)
```

The CSV file should have one user per row with columns for username, role, installations, and description (contact support for the exact column format).

---

### Grant Permissions

Grant a user access to a specific element.

```python
# Grant read permission
client.grant_basic_permission(
    user_id=501,
    element_code='METER_ZONE_A_01',
    write=False   # False = read only, True = read + write
)

# Grant write permission
client.grant_basic_permission(
    user_id=501,
    element_code='METER_ZONE_A_01',
    write=True
)
```

---

## API Key Management

API keys allow programmatic access without exposing a password.

### Generate a New API Key

```python
# Generates a new API key for the specified username
client.generate_api_key(
    username='john.doe',
    description='Integration server key'
)
```

### Revoke an API Key

```python
# Revoke the current API key for a user
client.revoke_api_key(username='john.doe')
```

### Generate a Secure Password

A helper utility to generate a strong random password (useful when creating new users programmatically):

```python
import hidroconta.api as demeter

password = demeter.generate_secure_password(length=16)
print(password)  # e.g. "aB3.xZ7mKqL1wP9s"
```

---

## Types Reference

Import types with:
```python
import hidroconta.types as tp
```

### Element Types

```python
tp.Element.RTU
tp.Element.COUNTER
tp.Element.ANALOG_INPUT
tp.Element.DIGITAL_INPUT
tp.Element.DIGITAL_OUTPUT
tp.Element.IRIS
tp.Element.IRIS_NBIOT
tp.Element.IRIS_LORAWAN
tp.Element.IRIS_SIGFOX
tp.Element.IRIS_3COM
tp.Element.HYDRANT
tp.Element.VALVE
tp.Element.CENTINEL
tp.Element.WMBUS_COUNTER
tp.Element.CENTAURUS
tp.Element.CENTAURUS_NBIOT
tp.Element.CENTAURUS_3COM
tp.Element.NAUTILUS_PLUS
```

### Historical Data Types

Each type has two attributes: `.subtype` (the category) and `.subcode` (the specific measurement).

| Type | Measurement |
|---|---|
| `tp.CounterGlobalHist` | Total accumulated volume from a counter |
| `tp.FlowHist` | Flow rate from a counter |
| `tp.AnalogInputHist` | Analog sensor reading (pressure, level, etc.) |
| `tp.IrisCounterGlobalHist` | Total volume from an IRIS device |
| `tp.NautilusPlusPositiveCounterGlobalHist` | Nautilus+ forward total volume |
| `tp.NautilusPlusNegativeCounterGlobalHist` | Nautilus+ reverse total volume |
| `tp.NautilusPlusNetCounterGlobalHist` | Nautilus+ net volume |
| `tp.NautilusPlusFlowRateHist` | Nautilus+ flow rate |
| `tp.NautilusPlusWaterTemperatureHist` | Nautilus+ water temperature |
| `tp.NautilusPlusPressureHist` | Nautilus+ pressure |
| `tp.CustomHist(subtype, subcode)` | Custom type with manual subtype/subcode |

### Status

```python
tp.Status.ENABLED   # Only active elements
tp.Status.DISABLED  # Only inactive elements
tp.Status.ALL       # All elements regardless of status
```

### Roles

```python
tp.Role.BASIC
tp.Role.INSTALLER
tp.Role.MANUFACTURING
tp.Role.ADMIN
tp.Role.ADVANCED
tp.Role.HIDROCONTA
```

---

## Error Handling

Always wrap your API calls in try/except blocks for production code.

### DemeterStatusCodeException

Raised when the server returns an unexpected HTTP status code (any code that is not 200 or 201).

```python
import hidroconta.api as demeter

with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    client.login(api_key='YOUR_API_KEY')

    try:
        df = client.get_rtus(element_id=99999, pandas=True)
    except demeter.DemeterStatusCodeException as e:
        print(f'API Error: {e}')
        # e contains the HTTP status code and error message from the server
```

### MfaRequiredException

Raised during login when the account has MFA enabled. Contains the `mfa_token` needed to complete authentication.

```python
try:
    client.login(username='user', password='pass')
except demeter.MfaRequiredException as mfa:
    # Complete the login with the MFA code
    client.validate_mfa_email(
        code=input('Enter your email OTP: '),
        mfa_token=mfa.mfa_token,
        device_nickname='My Script'
    )
```

### Verbose Mode (Debugging)

Enable verbose output to see detailed request/response information, useful for debugging:

```python
with demeter.DemeterClient(endpoints.Server.TEST) as client:
    client.set_verbose(True)   # Enable debug output
    client.login(api_key='YOUR_API_KEY')
    # All subsequent requests will print detailed logs
```

---

## Utilities

### Timestamp Formatting

The API uses a specific date format (`DD/MM/YYYY HH:MM:SS`). The `hidroconta.time` module handles conversions:

```python
import hidroconta.time as htime
import datetime

# Convert a datetime object to the API's string format
ts_string = htime.strftime_demeter(datetime.datetime.now())
# Result: '22/06/2026 14:30:00'

# Parse a timestamp string from the API into a datetime object
ts_datetime = htime.strptime_demeter('22/06/2026 14:30:00')
# Result: datetime.datetime(2026, 6, 22, 14, 30, 0)
```

---

## Full Working Example

A complete script showing the most common operations together:

```python
import datetime
import hidroconta.api as demeter
import hidroconta.types as tp
import hidroconta.endpoints as endpoints

# â”€â”€â”€ Connect â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
with demeter.DemeterClient(endpoints.Server.MAIN) as client:
    client.login(api_key='YOUR_API_KEY')

    # â”€â”€â”€ Search â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    print("--- Searching for elements ---")
    df_search = client.search(
        text='ZONE-A',
        element_types=[tp.Element.COUNTER, tp.Element.RTU],
        status=tp.Status.ENABLED,
        pandas=True
    )
    print(df_search)

    # â”€â”€â”€ Get all RTUs â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    print("\n--- All RTUs ---")
    df_rtus = client.get_rtus(pandas=True)
    print(df_rtus.head())

    # â”€â”€â”€ Get historical data â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    print("\n--- Historical counter data (last 24h) ---")
    end   = datetime.datetime.now()
    start = end - datetime.timedelta(days=1)

    df_hist = client.get_historics(
        start_date=start,
        end_date=end,
        element_ids=[1001, 1002],
        subtype=tp.CounterGlobalHist.subtype,
        subcode=[tp.CounterGlobalHist.subcode],
        pandas=True
    )
    print(df_hist)

    # â”€â”€â”€ Update a counter â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    print("\n--- Updating counter ---")
    try:
        client.global_update(
            rtu_id=12345,
            timestamp=datetime.datetime.now(),
            liters=1500000,
            position=0,
            expansion=0
        )
        print("Counter updated successfully.")
    except demeter.DemeterStatusCodeException as e:
        print(f"Failed to update counter: {e}")

    # â”€â”€â”€ Error handling example â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    print("\n--- Attempting to get a non-existent RTU ---")
    try:
        df = client.get_rtus(element_id=99999, pandas=True)
    except demeter.DemeterStatusCodeException as e:
        print(f"Expected error: {e}")
```

---

## Version History

| Version | Changes |
|---|---|
| **3.1.2** | Allows setting a custom position/expansion when creating a new demeter watermeter.
| **3.1.1** | Added method for getting criteria in any element type + Added serial number when creating a demeter watermeter.
| **3.1.0** | Added demeter watermeter creation.
| **3.0.0** | Full OOP refactor. API key login + MFA validation. **Not backwards-compatible.** |
| **2.0.0** | Added Nautilus+ device compatibility |
| **1.8.0** | Fixed history consumption endpoint for legacy values |
| **1.7.1** | Fixed error in `get_criteria` |
| **1.7.0** | Added `IrisCounterGlobalHist` type |
| **1.6.1** | Fixed `FlowlHist` typo (renamed to `FlowHist`) |
| **1.6.0** | Added `FlowHist` and `CustomHist` historical types |

---

*For further assistance, contact [javier.lopez@hidroconta.com](mailto:javier.lopez@hidroconta.com)*
