"""Core Growatt API client and helpers."""

# ruff: noqa: S324

import datetime
import hashlib
import json
import re
import secrets
import warnings
from enum import IntEnum

import requests

from .exceptions import GrowattError

name = "growattServer"

BATT_MODE_LOAD_FIRST = 0
BATT_MODE_BATTERY_FIRST = 1
BATT_MODE_GRID_FIRST = 2


def hash_password(password):
    """
    Return a modified MD5-like hex digest with 'c' substitutions.

    The algorithm computes an MD5 hex digest and replaces bytes with a '0'
    nibble by the character 'c' at every other position.
    """
    password_md5 = hashlib.md5(password.encode("utf-8")).hexdigest()  # nosec
    for i in range(0, len(password_md5), 2):
        if password_md5[i] == "0":
            password_md5 = password_md5[0:i] + "c" + password_md5[i + 1:]
    return password_md5


class Timespan(IntEnum):
    """Enumeration of supported timespans."""

    hour = 0
    day = 1
    month = 2


class GrowattApi:
    """Base client for Growatt API endpoints."""

    server_url = "https://openapi.growatt.com/"
    agent_identifier = "Dalvik/2.1.0 (Linux; U; Android 12; https://github.com/indykoning/PyPi_GrowattServer)"

    def __init__(self, add_random_user_id=False, agent_identifier=None) -> None:
        """
        Initialize the Growatt API client.

        Args:
            add_random_user_id: Append a short random suffix to the user-agent.
            agent_identifier: Optional override for the user-agent string.

        """
        if agent_identifier is not None:
            self.agent_identifier = agent_identifier

        # If a random user id is required, generate a 5 digit number and add it to the user agent
        if add_random_user_id:
            random_number = "".join(str(secrets.randbelow(10)) for _ in range(5))
            self.agent_identifier += " - " + random_number

        self.session = requests.Session()

        def _raise_for_status(response, *args: object, **kwargs: object) -> None:
            _ = args
            _ = kwargs
            response.raise_for_status()

        self.session.hooks = {"response": _raise_for_status}

        headers = {"User-Agent": self.agent_identifier}
        self.session.headers.update(headers)

    def __get_date_string(self, timespan=None, date=None):
        if timespan is not None and not isinstance(timespan, Timespan):
            raise ValueError("timespan must be a Timespan enum value")

        if date is None:
            date = datetime.datetime.now(datetime.UTC)

        date_str = ""
        if timespan == Timespan.month:
            date_str = date.strftime("%Y-%m")
        else:
            date_str = date.strftime("%Y-%m-%d")

        return date_str

    def get_url(self, page):
        """Return the page URL."""
        return self.server_url + page

    def login(self, username, password, is_password_hashed=False):
        """
        Log the user in.

        Returns
        -------
        'data' -- A List containing Objects containing the folowing
            'plantName' -- Friendly name of the plant
            'plantId'   -- The ID of the plant
        'service'
        'quality'
        'isOpenSmartFamily'
        'totalData' -- An Object
        'success'   -- True or False
        'msg'
        'app_code'
        'user' -- An Object containing a lot of user information
            'uid'
            'userLanguage'
            'inverterGroup' -- A List
            'timeZone' -- A Number
            'lat'
            'lng'
            'dataAcqList' -- A List
            'type'
            'accountName' -- The username
            'password' -- The password hash of the user
            'isValiPhone'
            'kind'
            'mailNotice' -- True or False
            'id'
            'lasLoginIp'
            'lastLoginTime'
            'userDeviceType'
            'phoneNum'
            'approved' -- True or False
            'area' -- Continent of the user
            'smsNotice' -- True or False
            'isAgent'
            'token'
            'nickName'
            'parentUserId'
            'customerCode'
            'country'
            'isPhoneNumReg'
            'createDate'
            'rightlevel'
            'appType'
            'serverUrl'
            'roleId'
            'enabled' -- True or False
            'agentCode'
            'inverterList' -- A list
            'email'
            'company'
            'activeName'
            'codeIndex'
            'appAlias'
            'isBigCustomer'
            'noticeType'

        """
        if not is_password_hashed:
            password = hash_password(password)

        response = self.session.post(self.get_url("newTwoLoginAPI.do"), data={
            "userName": username,
            "password": password
        })

        data = response.json()["back"]
        if data["success"]:
            data.update({
                "userId": data["user"]["id"],
                "userLevel": data["user"]["rightlevel"]
            })
        return data

    def plant_list(self, user_id):
        """
        Get a list of plants connected to this account.

        Args:
            user_id (str): The ID of the user.

        Returns:
            list: A list of plants connected to the account.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.get(
            self.get_url("PlantListAPI.do"),
            params={"userId": user_id},
            allow_redirects=False
        )

        return response.json().get("back", [])

    def plant_detail(self, plant_id, timespan, date=None):
        """
        Get plant details for specified timespan.

        Args:
            plant_id (str): The ID of the plant.
            timespan (Timespan): The ENUM value conforming to the time window you want e.g. hours from today, days, or months.
            date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now().

        Returns:
            dict: A dictionary containing the plant details.

        Raises:
            Exception: If the request to the server fails.

        """
        date_str = self.__get_date_string(timespan, date)

        response = self.session.get(self.get_url("PlantDetailAPI.do"), params={
            "plantId": plant_id,
            "type": timespan.value,
            "date": date_str
        })

        return response.json().get("back", {})

    def plant_list_two(self):
        """
        Get a list of all plants with detailed information.

        Returns:
            list: A list of plants with detailed information.

        """
        response = self.session.post(
            self.get_url("newTwoPlantAPI.do"),
            params={"op": "getAllPlantListTwo"},
            data={
                "language": "1",
                "nominalPower": "",
                "order": "1",
                "pageSize": "15",
                "plantName": "",
                "plantStatus": "",
                "toPageNum": "1"
            }
        )

        return response.json().get("PlantList", [])

    def inverter_data(self, inverter_id, date=None):
        """
        Get inverter data for specified date or today.

        Args:
            inverter_id (str): The ID of the inverter.
            date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now().

        Returns:
            dict: A dictionary containing the inverter data.

        Raises:
            Exception: If the request to the server fails.

        """
        date_str = self.__get_date_string(date=date)
        response = self.session.get(self.get_url("newInverterAPI.do"), params={
            "op": "getInverterData",
            "id": inverter_id,
            "type": 1,
            "date": date_str
        })

        return response.json()

    def inverter_detail(self, inverter_id):
        """
        Get detailed data from PV inverter.

        Args:
            inverter_id (str): The ID of the inverter.

        Returns:
            dict: A dictionary containing the inverter details.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.get(self.get_url("newInverterAPI.do"), params={
            "op": "getInverterDetailData",
            "inverterId": inverter_id
        })

        return response.json()

    def inverter_detail_two(self, inverter_id):
        """
        Get detailed data from PV inverter (alternative endpoint).

        Args:
            inverter_id (str): The ID of the inverter.

        Returns:
            dict: A dictionary containing the inverter details.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.get(self.get_url("newInverterAPI.do"), params={
            "op": "getInverterDetailData_two",
            "inverterId": inverter_id
        })

        return response.json()

    def tlx_system_status(self, plant_id, tlx_id):
        """
        Get status of the system.

        Args:
            plant_id (str): The ID of the plant.
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing system status.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.post(
            self.get_url("newTlxApi.do"),
            params={"op": "getSystemStatus_KW"},
            data={"plantId": plant_id,
                  "id": tlx_id}
        )

        return response.json().get("obj", {})

    def tlx_energy_overview(self, plant_id, tlx_id):
        """
        Get energy overview.

        Args:
            plant_id (str): The ID of the plant.
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing energy data.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.post(
            self.get_url("newTlxApi.do"),
            params={"op": "getEnergyOverview"},
            data={"plantId": plant_id,
                  "id": tlx_id}
        )

        return response.json().get("obj", {})

    def tlx_energy_prod_cons(self, plant_id, tlx_id, timespan=Timespan.hour, date=None):
        """
        Get energy production and consumption (kW).

        Args:
            plant_id: The plant identifier.
            tlx_id: The ID of the TLX inverter.
            timespan: Timespan enum for the requested range.
            date: Date of interest.

        Returns:
            dict: A dictionary containing energy data.

        Raises:
            Exception: If the request to the server fails.

        """
        date_str = self.__get_date_string(timespan, date)

        response = self.session.post(
            self.get_url("newTlxApi.do"),
            params={"op": "getEnergyProdAndCons_KW"},
            data={"date": date_str,
                  "plantId": plant_id,
                  "language": "1",
                  "id": tlx_id,
                  "type": timespan.value}
        )

        return response.json().get("obj", {})

    def tlx_data(self, tlx_id, date=None):
        """
        Get TLX inverter data for specified date or today.

        Args:
            tlx_id (str): The ID of the TLX inverter.
            date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now().

        Returns:
            dict: A dictionary containing the TLX inverter data.

        Raises:
            Exception: If the request to the server fails.

        """
        date_str = self.__get_date_string(date=date)
        response = self.session.get(self.get_url("newTlxApi.do"), params={
            "op": "getTlxData",
            "id": tlx_id,
            "type": 1,
            "date": date_str
        })

        return response.json()

    def tlx_detail(self, tlx_id):
        """
        Get detailed data from TLX inverter.

        Args:
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing the detailed TLX inverter data.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.get(self.get_url("newTlxApi.do"), params={
            "op": "getTlxDetailData",
            "id": tlx_id
        })

        return response.json()

    def tlx_params(self, tlx_id):
        """
        Get parameters for TLX inverter.

        Args:
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing the TLX inverter parameters.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.get(self.get_url("newTlxApi.do"), params={
            "op": "getTlxParams",
            "id": tlx_id
        })

        return response.json()

    def tlx_all_settings(self, tlx_id):
        """
        Get all possible settings from TLX inverter.

        Args:
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing all possible settings for the TLX inverter.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.post(self.get_url("newTlxApi.do"), params={
            "op": "getTlxSetData"
        }, data={
            "serialNum": tlx_id
        })

        return response.json().get("obj", {}).get("tlxSetBean")

    def tlx_enabled_settings(self, tlx_id):
        """
        Get "Enabled settings" from TLX inverter.

        Args:
            tlx_id (str): The ID of the TLX inverter.

        Returns:
            dict: A dictionary containing the enabled settings.

        Raises:
            Exception: If the request to the server fails.

        """
        string_time = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d")
        response = self.session.post(
            self.get_url("newLoginAPI.do"),
            params={"op": "getSetPass"},
            data={"deviceSn": tlx_id, "stringTime": string_time, "type": "5"}
        )

        return response.json().get("obj", {})

    def tlx_battery_info(self, serial_num):
        """
        Get battery information.

        Args:
            serial_num (str): The serial number of the battery.

        Returns:
            dict: A dictionary containing the battery information.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.post(
            self.get_url("newTlxApi.do"),
            params={"op": "getBatInfo"},
            data={"lan": 1, "serialNum": serial_num}
        )

        return response.json().get("obj", {})

    def tlx_battery_info_detailed(self, plant_id, serial_num):
        """
        Get detailed battery information.

        Args:
            plant_id (str): The ID of the plant.
            serial_num (str): The serial number of the battery.

        Returns:
            dict: A dictionary containing the detailed battery information.

        Raises:
            Exception: If the request to the server fails.

        """
        response = self.session.post(
            self.get_url("newTlxApi.do"),
            params={"op": "getBatDetailData"},
            data={"lan": 1, "plantId": plant_id, "id": serial_num}
        )

        return response.json()

    def mix_info(self, mix_id, plant_id=None):
        """
        Get high-level values from a Mix device.

        Args:
            mix_id: The device serial number.
            plant_id: Optional plant identifier.

        Returns:
            dict:
                'acChargeEnergyToday' -- ??? 2.7
                'acChargeEnergyTotal' -- ??? 25.3
                'acChargePower' -- ??? 0
                'capacity': '45' -- The current remaining capacity of the batteries (same as soc but without the % sign)
                'eBatChargeToday' -- Battery charged today in kWh
                'eBatChargeTotal' -- Battery charged total (all time) in kWh
                'eBatDisChargeToday' -- Battery discharged today in kWh
                'eBatDisChargeTotal' -- Battery discharged total (all time) in kWh
                'epvToday' -- Energy generated from PVs today in kWh
                'epvTotal' -- Energy generated from PVs total (all time) in kWh
                'isCharge'-- ??? 0 - Possible a 0/1 based on whether or not the battery is charging
                'pCharge1' -- ??? 0
                'pDischarge1' -- Battery discharging rate in W
                'soc' -- Statement of charge including % symbol
                'upsPac1' -- ??? 0
                'upsPac2' -- ??? 0
                'upsPac3' -- ??? 0
                'vbat' -- Battery Voltage
                'vbatdsp' -- ??? 51.8
                'vpv1' -- Voltage PV1
                'vpv2' -- Voltage PV2

        """
        request_params = {
            "op": "getMixInfo",
            "mixId": mix_id
        }

        if (plant_id):
            request_params["plantId"] = plant_id

        response = self.session.get(self.get_url(
            "newMixApi.do"), params=request_params)

        return response.json().get("obj", {})

    def mix_totals(self, mix_id, plant_id):
        """
        Get totals values from a Mix device.

        Args:
            mix_id: The device serial number.
            plant_id: Plant identifier.

        Returns:
            dict: Totals response object.
                'echargetoday' -- Battery charged today in kWh (same as eBatChargeToday from mix_info)
                'echargetotal' -- Battery charged total (all time) in kWh (same as eBatChargeTotal from mix_info)
                'edischarge1Today' -- Battery discharged today in kWh (same as eBatDisChargeToday from mix_info)
                'edischarge1Total' -- Battery discharged total (all time) in kWh (same as eBatDisChargeTotal from mix_info)
                'elocalLoadToday' -- Load consumption today in kWh
                'elocalLoadTotal' -- Load consumption total (all time) in kWh
                'epvToday' -- Energy generated from PVs today in kWh (same as epvToday from mix_info)
                'epvTotal' -- Energy generated from PVs total (all time) in kWh (same as epvTotal from mix_info)
                'etoGridToday' -- Energy exported to the grid today in kWh
                'etogridTotal' -- Energy exported to the grid total (all time) in kWh
                'photovoltaicRevenueToday' -- Revenue earned from PV today in 'unit' currency
                'photovoltaicRevenueTotal' -- Revenue earned from PV total (all time) in 'unit' currency
                'unit' -- Unit of currency for 'Revenue'

        """
        response = self.session.post(self.get_url("newMixApi.do"), params={
            "op": "getEnergyOverview",
            "mixId": mix_id,
            "plantId": plant_id
        })

        return response.json().get("obj", {})

    def mix_system_status(self, mix_id, plant_id):
        """
        Get current status from a Mix device.

        Args:
            mix_id: The device serial number.
            plant_id: Plant identifier.

        Returns:
            dict: Status response object.
                'SOC' -- Statement of charge (remaining battery %)
                'chargePower' -- Battery charging rate in kw
                'fAc' -- Frequency (Hz)
                'lost' -- System status e.g. 'mix.status.normal'
                'pLocalLoad' -- Load conumption in kW
                'pPv1' -- PV1 Wattage in W
                'pPv2' -- PV2 Wattage in W
                'pactogrid' -- Export to grid rate in kW
                'pactouser' -- Import from grid rate in kW
                'pdisCharge1' -- Discharging batteries rate in kW
                'pmax' -- ??? 6 ??? PV Maximum kW ??
                'ppv' -- PV combined Wattage in kW
                'priorityChoose' -- Priority setting - 0=Local load
                'status' -- System statue - ENUM - Unknown values
                'unit' -- Unit of measurement e.g. 'kW'
                'upsFac' -- ??? 0
                'upsVac1' -- ??? 0
                'uwSysWorkMode' -- ??? 6
                'vAc1' -- Grid voltage in V
                'vBat' -- Battery voltage in V
                'vPv1' -- PV1 voltage in V
                'vPv2' -- PV2 voltage in V
                'vac1' -- Grid voltage in V (same as vAc1)
                'wBatteryType' -- ??? 1

        """
        response = self.session.post(self.get_url("newMixApi.do"), params={
            "op": "getSystemStatus_KW",
            "mixId": mix_id,
            "plantId": plant_id
        })

        return response.json().get("obj", {})

    def mix_detail(self, mix_id, plant_id, timespan=Timespan.hour, date=None):
        """
        Get Mix details for the given timespan.

        Args:
            mix_id: Serial number (device_sn) of the inverter.
            plant_id: Plant identifier.
            timespan: Timespan enum for the requested range.
            date: Date of interest (defaults to now).

        Returns:
            dict: The response object containing mix details.
                A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively (below)
                'chartData': {   '00:05': {   'pacToGrid' -- Export rate to grid in kW
                                            'pacToUser' -- Import rate from grid in kW
                                            'pdischarge' -- Battery discharge in kW
                                            'ppv' -- Solar generation in kW
                                            'sysOut' -- Load consumption in kW
                                        },
                                '00:10': {   'pacToGrid': '0',
                                            'pacToUser': '0.93',
                                            'pdischarge': '0',
                                            'ppv': '0',
                                            'sysOut': '0.93'},
                                ......
                            }
                'eAcCharge' -- Exported to grid in kWh
                'eCharge' -- System production in kWh = Self-consumption + Exported to Grid
                'eChargeToday' -- Load consumption from solar in kWh
                'eChargeToday1' -- Self-consumption in kWh
                'eChargeToday2' -- Self-consumption in kWh (eChargeToday + echarge1)
                'echarge1' -- Load consumption from battery in kWh
                'echargeToat' -- Total battery discharged (all time) in kWh
                'elocalLoad' -- Load consumption in kW (battery + solar + imported)
                'etouser' -- Load consumption imported from grid in kWh
                'photovoltaic' -- Load consumption from solar in kWh (same as eChargeToday)
                'ratio1' -- % of system production that is self-consumed
                'ratio2' -- % of system production that is exported
                'ratio3' -- % of Load consumption that is "self consumption"
                'ratio4' -- % of Load consumption that is "imported from grid"
                'ratio5' -- % of Self consumption that is directly from Solar
                'ratio6' -- % of Self consumption that is from batteries
                'unit' -- Unit of measurement e.g kWh
                'unit2' -- Unit of measurement e.g kW


                NOTE - It is possible to calculate the PV generation that went into charging the batteries by performing the following calculation:
                Solar to Battery = Solar Generation - Export to Grid - Load consumption from solar
                                epvToday (from mix_info) - eAcCharge - eChargeToday

        """
        date_str = self.__get_date_string(timespan, date)

        response = self.session.post(
            self.get_url("newMixApi.do"),
            params={
                "op": "getEnergyProdAndCons_KW",
                "plantId": plant_id,
                "mixId": mix_id,
                "type": timespan.value,
                "date": date_str,
            },
        )

        return response.json().get("obj", {})

    def get_mix_inverter_settings(self, serial_number):
        """
        Get the inverter settings related to battery modes.

        Args:
            serial_number: -- The serial number (device_sn) of the inverter.

        Returns:
            dict: A dictionary of settings.

        """
        default_params = {
            "op": "getMixSetParams",
            "serialNum": serial_number,
            "kind": 0
        }
        response = self.session.get(self.get_url("newMixApi.do"), params=default_params)
        return response.json()

    def dashboard_data(self, plant_id, timespan=Timespan.hour, date=None):
        """
        Get dashboard data for a plant over a timespan.

        Args:
            plant_id: Plant identifier.
            timespan: Timespan enum for the requested range.
            date: Date of interest (defaults to now).

        Returns:
            dict: Dashboard chart and summary data.
                A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively (below)
                NOTE: The keys are interpreted differently, the examples below describe what they are used for in a 'Mix' system
                'chartData': {   '00:05': {   'pacToUser' -- Power from battery in kW
                                            'ppv' -- Solar generation in kW
                                            'sysOut' -- Load consumption in kW
                                            'userLoad' -- Export in kW
                                        },
                                '00:10': {   'pacToUser': '0',
                                            'ppv': '0',
                                            'sysOut': '0.7',
                                            'userLoad': '0'},
                                ......
                            }
                'chartDataUnit' -- Unit of measurement e.g. 'kW',
                'eAcCharge' -- Energy exported to the grid in kWh e.g. '20.5kWh' (not accurate for Mix systems)
                'eCharge' -- System production in kWh = Self-consumption + Exported to Grid e.g '23.1kWh' (not accurate for Mix systems - actually showing the total 'load consumption'
                'eChargeToday1' -- Self-consumption of PPV (possibly including excess diverted to batteries) in kWh e.g. '2.6kWh' (not accurate for Mix systems)
                'eChargeToday2' -- Total self-consumption (PPV consumption(eChargeToday2Echarge1) + Battery Consumption(echarge1)) e.g. '10.1kWh' (not accurate for Mix systems)
                'eChargeToday2Echarge1' -- Self-consumption of PPV only e.g. '0.8kWh' (not accurate for Mix systems)
                'echarge1' -- Self-consumption from Battery only e.g. '9.3kWh'
                'echargeToat' -- Not used on Dashboard view, likely to be total battery discharged e.g. '152.1kWh'
                'elocalLoad' -- Total load consumption (etouser + eChargeToday2) e.g. '20.3kWh', (not accurate for Mix systems)
                'etouser'-- Energy imported from grid today (includes both directly used by load and AC battery charging e.g. '10.2kWh'
                'keyNames' -- Keys to be used for the graph data e.g. ['Solar', 'Load Consumption', 'Export To Grid', 'From Battery']
                'photovoltaic' -- Same as eChargeToday2Echarge1 e.g. '0.8kWh'
                'ratio1' -- % of 'Solar production' that is self-consumed e.g. '11.3%' (not accurate for Mix systems)
                'ratio2' -- % of 'Solar production' that is exported e.g. '88.7%' (not accurate for Mix systems)
                'ratio3' -- % of 'Load consumption' that is self consumption e.g. '49.8%' (not accurate for Mix systems)
                'ratio4' -- % of 'Load consumption' that is imported from the grid e.g '50.2%' (not accurate for Mix systems)
                'ratio5' -- % of Self consumption that is from batteries e.g. '92.1%' (not accurate for Mix systems)
                'ratio6' -- % of Self consumption that is directly from Solar e.g. '7.9%' (not accurate for Mix systems)

                NOTE: Does not return any data for a tlx system. Use plant_energy_data() instead.

        """
        date_str = self.__get_date_string(timespan, date)

        response = self.session.post(self.get_url("newPlantAPI.do"), params={
            "action": "getEnergyStorageData",
            "date": date_str,
            "type": timespan.value,
            "plantId": plant_id,
        })
        return response.json()

    def plant_settings(self, plant_id):
        """
        Get a dictionary containing the settings for the specified plant.

        Args:
            plant_id: The id of the plant you want the settings of

        Returns:
            dict: A python dictionary containing the settings for the specified plant.

        """
        response = self.session.get(self.get_url("newPlantAPI.do"), params={
            "op": "getPlant",
            "plantId": plant_id
        })

        return response.json()

    def storage_detail(self, storage_id):
        """Get "All parameters" from battery storage."""
        response = self.session.get(self.get_url("newStorageAPI.do"), params={
            "op": "getStorageInfo_sacolar",
            "storageId": storage_id
        })

        return response.json()

    def storage_params(self, storage_id):
        """Get much more detail from battery storage."""
        response = self.session.get(self.get_url("newStorageAPI.do"), params={
            "op": "getStorageParams_sacolar",
            "storageId": storage_id
        })

        return response.json()

    def storage_energy_overview(self, plant_id, storage_id):
        """Get some energy/generation overview data."""
        response = self.session.post(self.get_url("newStorageAPI.do?op=getEnergyOverviewData_sacolar"), params={
            "plantId": plant_id,
            "storageSn": storage_id
        })

        return response.json().get("obj", {})

    def inverter_list(self, plant_id):
        """Use device_list, it's more descriptive since the list contains more than inverters."""
        warnings.warn(
            "This function may be deprecated in the future because naming is not correct, use device_list instead", DeprecationWarning, stacklevel=2)
        return self.device_list(plant_id)

    def __get_all_devices(self, plant_id):
        """Get basic plant information with device list."""
        response = self.session.get(self.get_url("newTwoPlantAPI.do"),
                                    params={"op": "getAllDeviceList",
                                            "plantId": plant_id,
                                            "language": 1})

        return response.json().get("deviceList", {})

    def device_list(self, plant_id):
        """Get a list of all devices connected to plant."""
        device_list = self.plant_info(plant_id).get("deviceList", [])

        if not device_list:
            # for tlx systems, the device_list in plant is empty, so use __get_all_devices() instead
            device_list = self.__get_all_devices(plant_id)

        return device_list

    def plant_info(self, plant_id):
        """Get basic plant information with device list."""
        response = self.session.get(self.get_url("newTwoPlantAPI.do"), params={
            "op": "getAllDeviceListTwo",
            "plantId": plant_id,
            "pageNum": 1,
            "pageSize": 1
        })

        return response.json()

    def plant_energy_data(self, plant_id):
        """Get the energy data used in the 'Plant' tab in the phone."""
        response = self.session.post(self.get_url("newTwoPlantAPI.do"),
                                     params={
                                         "op": "getUserCenterEnertyDataByPlantid"},
                                     data={"language": 1,
                                           "plantId": plant_id})

        return response.json()

    def is_plant_noah_system(self, plant_id):
        """
        Check whether a plant is a Noah system.

        Args:
            plant_id: The id of the plant.

        Returns:
            dict: API response indicating Noah configuration.
                'msg'
                'result'    -- True or False
                'obj'   -- An Object containing if noah devices are configured
                    'isPlantNoahSystem' -- Is the specified plant a noah system (True or False)
                    'plantId'   -- The ID of the plant
                    'isPlantHaveNoah'   -- Are noah devices configured in the specified plant (True or False)
                    'deviceSn'  -- Serial number of the configured noah device
                    'plantName' -- Friendly name of the plant

        """
        response = self.session.post(self.get_url("noahDeviceApi/noah/isPlantNoahSystem"), data={
            "plantId": plant_id
        })
        return response.json()

    def noah_system_status(self, serial_number):
        """
        Get the Noah device status.

        Args:
            serial_number: Noah device serial number.

        Returns:
            dict: Status response object.
                'msg'
                'result'    -- True or False
                'obj' -- An Object containing the noah device status
                    'chargePower'   -- Battery charging rate in watt e.g. '200Watt'
                    'workMode'  -- Workingmode of the battery (0 = Load First, 1 = Battery First)
                    'soc'   -- Statement of charge (remaining battery %)
                    'associatedInvSn'   -- ???
                    'batteryNum'    -- Numbers of batterys
                    'profitToday'   -- Today generated profit through noah device
                    'plantId'   -- The ID of the plant
                    'disChargePower'    -- Battery discharging rate in watt e.g. '200Watt'
                    'eacTotal'  -- Total energy exported to the grid in kWh e.g. '20.5kWh'
                    'eacToday'  -- Today energy exported to the grid in kWh e.g. '20.5kWh'
                    'pac'   -- Export to grid rate in watt e.g. '200Watt'
                    'ppv'   -- Solar generation in watt e.g. '200Watt'
                    'alias' -- Friendly name of the noah device
                    'profitTotal'   -- Total generated profit through noah device
                    'moneyUnit' -- Unit of currency e.g. '€'
                    'status'    -- Is the noah device online (True or False)

        """
        response = self.session.post(self.get_url("noahDeviceApi/noah/getSystemStatus"), data={
            "deviceSn": serial_number
        })
        return response.json()

    def noah_info(self, serial_number):
        """
        Get detailed Noah device information.

        Args:
            serial_number: Noah device serial number.

        Returns:
            dict: Detailed Noah device info.
                'msg'
                'result'    -- True or False
                'obj' -- An Object containing the noah device informations
                    'neoList'   -- A List containing Objects
                    'unitList'  -- A Object containing currency units e.g. "Euro": "euro", "DOLLAR": "dollar"
                    'noah'  -- A Object containing the folowing
                        'time_segment'  -- A List containing Objects with configured "Operation Mode"
                            NOTE: The keys are generated numerical, the values are generated with folowing syntax "[workingmode (0 = Load First, 1 = Battery First)]_[starttime]_[endtime]_[output power]"
                            'time_segment': {
                                'time_segment1': "0_0:0_8:0_150", ([Load First]_[00:00]_[08:00]_[150 watt])
                                'time_segment2': "1_8:0_18:0_0", ([Battery First]_[08:00]_[18:00]_[0 watt])
                                ....
                            }
                        'batSns'    -- A List containing all battery Serial Numbers
                        'associatedInvSn'   -- ???
                        'plantId'   -- The ID of the plant
                        'chargingSocHighLimit'  -- Configured "Battery Management" charging upper limit
                        'chargingSocLowLimit'   -- Configured "Battery Management" charging lower limit
                        'defaultPower'  -- Configured "System Default Output Power"
                        'version'   -- The Firmware Version of the noah device
                        'deviceSn'  -- The Serial number of the noah device
                        'formulaMoney'  -- Configured "Select Currency" energy cost per kWh e.g. '0.22'
                        'alias' -- Friendly name of the noah device
                        'model' -- Model Name of the noah device
                        'plantName' -- Friendly name of the plant
                        'tempType'  -- ???
                        'moneyUnitText' -- Configured "Select Currency" (Value from the unitList) e.G. "euro"
                    'plantList' -- A List containing Objects containing the folowing
                        'plantId'   -- The ID of the plant
                        'plantImgName'  -- Friendly name of the plant Image
                        'plantName' -- Friendly name of the plant

        """
        response = self.session.post(self.get_url("noahDeviceApi/noah/getNoahInfoBySn"), data={
            "deviceSn": serial_number
        })
        return response.json()

    def update_plant_settings(self, plant_id, changed_settings, current_settings=None):
        """
        Update plant settings.

        Args:
            plant_id: Plant identifier.
            changed_settings: Dict of settings to change.
            current_settings: Current settings dict or None.

        Returns:
            dict: Server response indicating success or failure.

        """
        # If no existing settings have been provided then get them from the growatt server
        if current_settings is None:
            current_settings = self.plant_settings(plant_id)

        # These are the parameters that the form requires, without these an error is thrown. Pre-populate their values with the current values
        form_settings = {
            "plantCoal": (None, str(current_settings["formulaCoal"])),
            "plantSo2": (None, str(current_settings["formulaSo2"])),
            "accountName": (None, str(current_settings["userAccount"])),
            "plantID": (None, str(current_settings["id"])),
            # Hardcoded to 0 as I can't work out what value it should have
            "plantFirm": (None, "0"),
            "plantCountry": (None, str(current_settings["country"])),
            "plantType": (None, str(current_settings["plantType"])),
            "plantIncome": (None, str(current_settings["formulaMoneyStr"])),
            "plantAddress": (None, str(current_settings["plantAddress"])),
            "plantTimezone": (None, str(current_settings["timezone"])),
            "plantLng": (None, str(current_settings["plant_lng"])),
            "plantCity": (None, str(current_settings["city"])),
            "plantCo2": (None, str(current_settings["formulaCo2"])),
            "plantMoney": (None, str(current_settings["formulaMoneyUnitId"])),
            "plantPower": (None, str(current_settings["nominalPower"])),
            "plantLat": (None, str(current_settings["plant_lat"])),
            "plantDate": (None, str(current_settings["createDateText"])),
            "plantName": (None, str(current_settings["plantName"])),
        }

        # Overwrite the current value of the setting with the new value
        for setting, value in changed_settings.items():
            form_settings[setting] = (None, str(value))

        response = self.session.post(self.get_url(
            "newTwoPlantAPI.do?op=updatePlant"), files=form_settings)

        return response.json()

    def update_inverter_setting(self, serial_number, setting_type,
                                default_parameters, parameters):
        """
        Apply inverter settings.

        Args:
            serial_number: Serial number of the inverter.
            setting_type: Type of setting to configure.
            default_parameters: Default parameter mapping for the request. This
                    should contain the required keys for the specific endpoint
                    (commonly keys like ``op``, ``serialNum`` and ``type``).
            parameters: Parameters to send. May be either a ``dict`` mapping
                    parameter names to values, or a ``list`` of values. If a
                    ``list`` is supplied it will be converted to a dictionary of
                    the form ``{"param1": value1, "param2": value2, ...}``.

        Notes:
            - The function merges ``default_parameters`` with the provided
                ``parameters`` and issues a POST request to ``newTcpsetAPI.do``.
            - For Mix/AC/other inverter types the caller may wrap this helper
                with specific defaults (see ``update_mix_inverter_setting`` and
                ``update_ac_inverter_setting``).

        Returns:
            dict: Server response JSON.

        """
        # Ensure declared but unused args are referenced to satisfy linters
        _ = serial_number
        _ = setting_type
        settings_parameters = parameters

        # If we've been passed an array then convert it into a dictionary
        if isinstance(parameters, list):
            settings_parameters = {}
            for index, param in enumerate(parameters, start=1):
                settings_parameters["param" + str(index)] = param

        settings_parameters = {**default_parameters, **settings_parameters}

        response = self.session.post(self.get_url("newTcpsetAPI.do"),
                                     params=settings_parameters)

        return response.json()

    def update_mix_inverter_setting(self, serial_number, setting_type, parameters):
        """
        Set inverter parameters for a Mix inverter.

        Args:
            serial_number: Inverter serial number.
            setting_type: Setting type.
            parameters: Parameters to send.

        Returns:
            dict: Server response JSON.

        """
        default_parameters = {
            "op": "mixSetApiNew",
            "serialNum": serial_number,
            "type": setting_type
        }
        return self.update_inverter_setting(serial_number, setting_type,
                                            default_parameters, parameters)

    def update_ac_inverter_setting(self, serial_number, setting_type, parameters):
        """
        Set inverter parameters for an AC-coupled inverter.

        Args:
            serial_number: Inverter serial number.
            setting_type: Setting type.
            parameters: Parameters to send.

        Returns:
            dict: Server response JSON.

        """
        default_parameters = {
            "op": "spaSetApi",
            "serialNum": serial_number,
            "type": setting_type
        }
        return self.update_inverter_setting(serial_number, setting_type,
                                            default_parameters, parameters)

    def update_tlx_inverter_time_segment(self, serial_number, segment_id, batt_mode, start_time, end_time, enabled):
        """
        Update a TLX inverter time segment.

        Args:
            serial_number: Inverter serial number.
            segment_id: ID of the time segment.
            batt_mode: Battery mode.
            start_time: Segment start time (datetime.time).
            end_time: Segment end time (datetime.time).
            enabled: Whether the segment is enabled.

        Returns:
            dict: Server JSON response.

        """
        params = {
            "op": "tlxSet"
        }
        data = {
            "serialNum": serial_number,
            "type": f"time_segment{segment_id}",
            "param1": batt_mode,
            "param2": start_time.strftime("%H"),
            "param3": start_time.strftime("%M"),
            "param4": end_time.strftime("%H"),
            "param5": end_time.strftime("%M"),
            "param6": "1" if enabled else "0"
        }

        response = self.session.post(self.get_url(
            "newTcpsetAPI.do"), params=params, data=data)
        result = response.json()

        if not result.get("success", False):
            msg = f"Failed to update TLX inverter time segment: {result.get('msg', 'Unknown error')}"
            raise GrowattError(msg)

        return result

    def update_tlx_inverter_setting(self, serial_number, setting_type, parameter):
        """
        Set parameters on a TLX inverter.

        Args:
            serial_number: Inverter serial number.
            setting_type: Setting type to configure.
            parameter: Parameter(s) to send (dict, list or single value).

        Returns:
            dict: Server JSON response.

        """
        default_parameters = {
            "op": "tlxSet",
            "serialNum": serial_number,
            "type": setting_type
        }

        # If parameter is a single value, convert it to a dictionary
        if not isinstance(parameter, (dict, list)):
            parameter = {"param1": parameter}
        elif isinstance(parameter, list):
            parameter = {f"param{index+1}": param for index,
                         param in enumerate(parameter)}

        return self.update_inverter_setting(serial_number, setting_type,
                                            default_parameters, parameter)

    def update_noah_settings(self, serial_number, setting_type, parameters):
        """
        Apply settings for a Noah device.

        Args:
            serial_number: Noah device serial number.
            setting_type: Setting to be configured.
            parameters: Parameters to send (dict or list).

        Returns:
            dict: Server JSON response.

        """
        default_parameters = {
            "serialNum": serial_number,
            "type": setting_type
        }
        settings_parameters = parameters

        # If we've been passed an array then convert it into a dictionary
        if isinstance(parameters, list):
            settings_parameters = {}
            for index, param in enumerate(parameters, start=1):
                settings_parameters["param" + str(index)] = param

        settings_parameters = {**default_parameters, **settings_parameters}

        response = self.session.post(self.get_url("noahDeviceApi/noah/set"),
                                     data=settings_parameters)

        return response.json()

    def classic_inverter_info(self, device_sn):
        """
        Get classic inverter information by scraping the inverter settings page.

        The Growatt server does not provide a JSON API for classic inverter status,
        so this method fetches the HTML settings page and extracts the inverter
        data from an embedded JSON object in the JavaScript.

        Args:
            device_sn: The serial number of the inverter.

        Returns:
            dict: A dictionary containing the inverter information.
                'innerVersion'
                'timezone'
                'isBig'
                'voltageHighLimit' -- High voltage limit e.g. '263.0'
                'wideVoltageEnable'
                'reactiveRate'
                'modelText'
                'haveAfci'
                'activeRate' -- Active power rate e.g. '100'
                'lost'
                'alias' -- Friendly name of the inverter
                'datalogSn' -- Serial number of the datalogger
                'sysTime' -- System time e.g. '2026-03-01 10:02:45'
                'fwVersion' -- Firmware version e.g. 'AH1.0'
                'model' -- Model number
                'sn' -- Serial number of the inverter
                'pvPfCmdMemoryState'
                'onOff' -- Inverter on/off status ('0' = off, '1' = on)
                'voltageLowLimit' -- Low voltage limit e.g. '186.0'
                'plantId' -- The ID of the plant
                'pfModel'
                'workingFrequencyMin' -- Minimum working frequency e.g. '47.53'
                'nominalPower' -- Nominal power in watts e.g. '3600'
                'workingFrequencyMax' -- Maximum working frequency e.g. '51.5'
                'pf' -- Power factor e.g. '1.0'
                'location' -- Location string
                'deviceModel' -- Device model name e.g. 'GROWATT 3000MTL-S'
                'status' -- Inverter status code
                'lastUpdateTime' -- Last data update time

        Raises:
            GrowattError: If the inverter data cannot be extracted from the response.

        """
        response = self.session.get(
            self.get_url("commonDeviceSetC/setInverter"),
            params={"type": "server", "invSn": device_sn},
        )

        match = re.search(r"inv=JSON\.parse\('(\{.*?\})'\)", response.text)
        if not match:
            msg = f"Could not find inverter data in response for device {device_sn}"
            raise GrowattError(msg)

        try:
            return json.loads(match.group(1))
        except json.JSONDecodeError as err:
            msg = f"Failed to parse inverter data JSON for device {device_sn}"
            raise GrowattError(msg) from err

    def update_classic_inverter_setting(self, default_parameters, parameters):
        """
        Apply classic inverter settings.

        Args:
            default_parameters: Default parameters dict. For classic inverters
                this commonly contains keys such as "action": "inverterSet"
                and "serialNum": <device_sn> (see examples/settings_example_classic.py).
            parameters: Parameters to send. Two common forms are accepted:
                - dict: mapping of parameter names to values (e.g. {"param1": "..."}).
                - list: positional values which will be converted to
                  {"param1": v1, "param2": v2, ...}.

        Example:
            The classic inverter settings example uses a parameter structure
            like::

                default_parameters = {
                    "action": "inverterSet",
                    "serialNum": device_sn,
                }

                # Example A: toggle PV on/off
                parameters = {
                    "paramId": "pv_on_off",
                    "command_1": "0001",  # 0001 to turn on, 0000 to turn off
                    "command_2": "",
                }

                # Example B: set active PV power percentage
                parameters = {
                    "paramId": "pv_active_p_rate",
                    "command_1": "100",  # percentage (0-100)
                    "command_2": "",
                }

                # Example C: set reactive PV power percentage
                parameters = {
                    "paramId": "pv_reactive_p_rate",
                    "command_1": "100",  # percentage (0-100)
                    "command_2": "over", # "over" for Inductive, "under" for Capacitive
                }

                # Example D: set time
                parameters = {
                    "paramId": "pf_sys_year",
                    "command_1": "2026-01-01 20:00:00",  # Time in "YYYY-MM-DD HH:MM:SS" format
                    "command_2": "",
                }

                # Example E: set Powerfactor (PF)
                parameters = {
                    "paramId": "pv_power_factor",
                    "command_1": "1.0",  # PF Value (-0.8 ~ -1/0.8 ~ 1)
                    "command_2": "",
                }

                # Example F: Set Grid Voltage High
                parameters = {
                    "paramId": "pv_grid_voltage_high",
                    "command_1": "263.0",  # Voltage in volts
                    "command_2": "",
                }

                # Example G: Set Grid Voltage Low
                parameters = {
                    "paramId": "pv_grid_voltage_low",
                    "command_1": "186.0",  # Voltage in volts
                    "command_2": "",
                }

            This method will POST the merged parameters to "tcpSet.do".

        Returns:
            dict: Server JSON response.

        """
        settings_parameters = parameters

        # If we've been passed an array then convert it into a dictionary
        if isinstance(parameters, list):
            settings_parameters = {}
            for index, param in enumerate(parameters, start=1):
                settings_parameters["param" + str(index)] = param

        settings_parameters = {**default_parameters, **settings_parameters}

        response = self.session.post(self.get_url("tcpSet.do"),
                                     params=settings_parameters)

        return response.json()
