asset_grouping.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. """
  2. Convenience functions and class for accessing generic assets in groups.
  3. For example, group by asset type or by location.
  4. """
  5. from __future__ import annotations
  6. import inflect
  7. from sqlalchemy import select, Select
  8. from flexmeasures.data import db
  9. from flexmeasures.data.queries.generic_assets import (
  10. get_asset_group_queries as get_asset_group_queries_new,
  11. )
  12. from flexmeasures.utils.coding_utils import deprecated
  13. from flexmeasures.utils.flexmeasures_inflection import parameterize
  14. from flexmeasures.data.models.generic_assets import (
  15. GenericAssetType,
  16. GenericAsset,
  17. assets_share_location,
  18. )
  19. p = inflect.engine()
  20. @deprecated(get_asset_group_queries_new)
  21. def get_asset_group_queries(
  22. group_by_type: bool = True,
  23. group_by_account: bool = False,
  24. group_by_location: bool = False,
  25. custom_aggregate_type_groups: dict[str, list[str]] | None = None,
  26. ) -> dict[str, Select]:
  27. """
  28. An asset group is defined by Asset queries, which this function can generate.
  29. Each query has a name (for the asset group it represents).
  30. These queries still need an executive call, like all(), count() or first().
  31. This function limits the assets to be queried to the current user's account,
  32. if the user is not an admin.
  33. Note: Make sure the current user has the "read" permission on their account (on GenericAsset.__class__?? See https://github.com/FlexMeasures/flexmeasures/issues/200) or is an admin.
  34. :param group_by_type: If True, groups will be made for assets with the same type. We prefer pluralised group names here. Defaults to True.
  35. :param group_by_account: If True, groups will be made for assets within the same account. This makes sense for admins, as they can query across accounts.
  36. :param group_by_location: If True, groups will be made for assets at the same location. Naming of the location currently supports charge points (for EVSEs).
  37. :param custom_aggregate_type_groups: dict of asset type groupings (mapping group names to names of asset types). See also the setting FLEXMEASURES_ASSET_TYPE_GROUPS.
  38. """
  39. return get_asset_group_queries_new(
  40. group_by_type, group_by_account, group_by_location, custom_aggregate_type_groups
  41. )
  42. class AssetGroup:
  43. """
  44. This class represents a group of assets of the same type, offering some convenience functions
  45. for displaying their properties.
  46. When initialised with an asset type name, the group will contain all assets of
  47. the given type that are accessible to the current user's account.
  48. When initialised with a query for GenericAssets, as well, the group will list the assets returned by that query. This can be useful in combination with get_asset_group_queries,
  49. see above.
  50. TODO: On a conceptual level, we can model two functionally useful ways of grouping assets:
  51. - AggregatedAsset if it groups assets of only 1 type,
  52. - GeneralizedAsset if it groups assets of multiple types
  53. There might be specialised subclasses, as well, for certain groups, like a market and consumers.
  54. """
  55. name: str
  56. assets: list[GenericAsset]
  57. count: int
  58. unique_asset_types: list[GenericAssetType]
  59. unique_asset_type_names: list[str]
  60. def __init__(self, name: str, asset_query: Select | None = None):
  61. """The asset group name is either the name of an asset group or an individual asset."""
  62. if name is None or name == "":
  63. raise Exception("Empty asset (group) name passed (%s)" % name)
  64. self.name = name
  65. if asset_query is None:
  66. asset_query = select(GenericAsset).filter_by(name=self.name)
  67. # List unique asset types and asset type names represented by this group
  68. self.assets = db.session.scalars(asset_query).all()
  69. self.unique_asset_types = list(set([a.asset_type for a in self.assets]))
  70. self.unique_asset_type_names = list(
  71. set([a.asset_type.name for a in self.assets])
  72. )
  73. # Count all assets that are identified by this group's name
  74. self.count = len(self.assets)
  75. @property
  76. def is_unique_asset(self) -> bool:
  77. """Determines whether the resource represents a unique asset."""
  78. return [self.name] == [a.name for a in self.assets]
  79. @property
  80. def display_name(self) -> str:
  81. """Attempt to get a beautiful name to show if possible."""
  82. if self.is_unique_asset:
  83. return self.assets[0].name
  84. return self.name
  85. def is_eligible_for_comparing_individual_traces(self, max_traces: int = 7) -> bool:
  86. """
  87. Decide whether comparing individual traces for assets in this asset group
  88. is a useful feature.
  89. The number of assets that can be compared is parametrizable with max_traces.
  90. Plot colors are reused if max_traces > 7, and run out if max_traces > 105.
  91. """
  92. return len(self.assets) in range(2, max_traces + 1) and assets_share_location(
  93. self.assets
  94. )
  95. @property
  96. def hover_label(self) -> str | None:
  97. """Attempt to get a hover label to show if possible."""
  98. label = p.join(
  99. [
  100. asset_type.description
  101. for asset_type in self.unique_asset_types
  102. if asset_type.description is not None
  103. ]
  104. )
  105. return label if label else None
  106. @property
  107. def parameterized_name(self) -> str:
  108. """Get a parametrized name for use in javascript."""
  109. return parameterize(self.name)
  110. def __str__(self):
  111. return self.display_name