import paddle
from paddle import ParamAttr
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.nn.initializer import Normal, Constant
from paddlex.ppdet.core.workspace import register
from paddlex.ppdet.modeling import ops
from paddlex.ppdet.modeling import bbox_utils
from paddlex.ppdet.modeling.proposal_generator.target_layer import RBoxAssigner
from ..cls_utils import _get_class_default_kwargs
import numpy as np

class S2ANetAnchorGenerator(nn.Layer):
    """
    AnchorGenerator by paddle
    """

    def __init__(self, base_size, scales, ratios, scale_major=True, ctr=None):
        super(S2ANetAnchorGenerator, self).__init__()
        self.base_size = base_size
        self.scales = paddle.to_tensor(scales)
        self.ratios = paddle.to_tensor(ratios)
        self.scale_major = scale_major
        self.ctr = ctr
        self.base_anchors = self.gen_base_anchors()

    @property
    def num_base_anchors(self):
        return self.base_anchors.shape[0]

    def gen_base_anchors(self):
        w = self.base_size
        h = self.base_size
        if self.ctr is None:
            x_ctr = 0.5 * (w - 1)
            y_ctr = 0.5 * (h - 1)
        else:
            x_ctr, y_ctr = self.ctr

        h_ratios = paddle.sqrt(self.ratios)
        w_ratios = 1 / h_ratios
        if self.scale_major:
            ws = (w * w_ratios[:] * self.scales[:]).reshape([-1])
            hs = (h * h_ratios[:] * self.scales[:]).reshape([-1])
        else:
            ws = (w * self.scales[:] * w_ratios[:]).reshape([-1])
            hs = (h * self.scales[:] * h_ratios[:]).reshape([-1])

        base_anchors = paddle.stack(
            [
                x_ctr - 0.5 * (ws - 1), y_ctr - 0.5 * (hs - 1),
                x_ctr + 0.5 * (ws - 1), y_ctr + 0.5 * (hs - 1)
            ],
            axis=-1)
        base_anchors = paddle.round(base_anchors)
        return base_anchors

    def _meshgrid(self, x, y, row_major=True):
        yy, xx = paddle.meshgrid(y, x)
        yy = yy.reshape([-1])
        xx = xx.reshape([-1])
        if row_major:
            return xx, yy
        else:
            return yy, xx

    def forward(self, featmap_size, stride=16):
        # featmap_size*stride project it to original area

        feat_h = featmap_size[0]
        feat_w = featmap_size[1]
        shift_x = paddle.arange(0, feat_w, 1, 'int32') * stride
        shift_y = paddle.arange(0, feat_h, 1, 'int32') * stride
        shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
        shifts = paddle.stack(
            [shift_xx, shift_yy, shift_xx, shift_yy], axis=-1)

        all_anchors = self.base_anchors[:, :] + shifts[:, :]
        all_anchors = all_anchors.reshape([feat_h * feat_w, 4])
        return all_anchors

    def valid_flags(self, featmap_size, valid_size):
        feat_h, feat_w = featmap_size
        valid_h, valid_w = valid_size
        assert valid_h <= feat_h and valid_w <= feat_w
        valid_x = paddle.zeros([feat_w], dtype='int32')
        valid_y = paddle.zeros([feat_h], dtype='int32')
        valid_x[:valid_w] = 1
        valid_y[:valid_h] = 1
        valid_xx, valid_yy = self._meshgrid(valid_x, valid_y)
        valid = valid_xx & valid_yy
        valid = paddle.reshape(valid, [-1, 1])
        valid = paddle.expand(valid, [-1, self.num_base_anchors]).reshape([-1])
        return valid

class AlignConv(nn.Layer):
    def __init__(self, in_channels, out_channels, kernel_size=3, groups=1):
        super(AlignConv, self).__init__()
        self.kernel_size = kernel_size
        self.align_conv = paddle.vision.ops.DeformConv2D(
            in_channels,
            out_channels,
            kernel_size=self.kernel_size,
            padding=(self.kernel_size - 1) // 2,
            groups=groups,
            weight_attr=ParamAttr(initializer=Normal(0, 0.01)),
            bias_attr=None)

    @paddle.no_grad()
    def get_offset(self, anchors, featmap_size, stride):
        """
        Args:
            anchors: [M,5] xc,yc,w,h,angle
            featmap_size: (feat_h, feat_w)
            stride: 8
        Returns:

        """
        anchors = paddle.reshape(anchors, [-1, 5])  # (NA,5)
        dtype = anchors.dtype
        feat_h = featmap_size[0]
        feat_w = featmap_size[1]
        pad = (self.kernel_size - 1) // 2
        idx = paddle.arange(-pad, pad + 1, dtype=dtype)

        yy, xx = paddle.meshgrid(idx, idx)
        xx = paddle.reshape(xx, [-1])
        yy = paddle.reshape(yy, [-1])

        # get sampling locations of default conv
        xc = paddle.arange(0, feat_w, dtype=dtype)
        yc = paddle.arange(0, feat_h, dtype=dtype)
        yc, xc = paddle.meshgrid(yc, xc)

        xc = paddle.reshape(xc, [-1, 1])
        yc = paddle.reshape(yc, [-1, 1])
        x_conv = xc + xx
        y_conv = yc + yy

        # get sampling locations of anchors
        # x_ctr, y_ctr, w, h, a = np.unbind(anchors, dim=1)
        x_ctr = anchors[:, 0]
        y_ctr = anchors[:, 1]
        w = anchors[:, 2]
        h = anchors[:, 3]
        a = anchors[:, 4]

        x_ctr = paddle.reshape(x_ctr, [-1, 1])
        y_ctr = paddle.reshape(y_ctr, [-1, 1])
        w = paddle.reshape(w, [-1, 1])
        h = paddle.reshape(h, [-1, 1])
        a = paddle.reshape(a, [-1, 1])

        x_ctr = x_ctr / stride
        y_ctr = y_ctr / stride
        w_s = w / stride
        h_s = h / stride
        cos, sin = paddle.cos(a), paddle.sin(a)
        dw, dh = w_s / self.kernel_size, h_s / self.kernel_size
        x, y = dw * xx, dh * yy
        xr = cos * x - sin * y
        yr = sin * x + cos * y
        x_anchor, y_anchor = xr + x_ctr, yr + y_ctr
        # get offset filed
        offset_x = x_anchor - x_conv
        offset_y = y_anchor - y_conv
        offset = paddle.stack([offset_y, offset_x], axis=-1)
        offset = paddle.reshape(
            offset,
            [feat_h * feat_w, self.kernel_size * self.kernel_size * 2])
        offset = paddle.transpose(offset, [1, 0])
        offset = paddle.reshape(
            offset,
            [1, self.kernel_size * self.kernel_size * 2, feat_h, feat_w])
        return offset

    def forward(self, x, refine_anchors, featmap_size, stride):
        offset = self.get_offset(refine_anchors, featmap_size, stride)
        x = F.relu(self.align_conv(x, offset))
        return x

@register
class S2ANetHead(nn.Layer):
    """
    S2Anet head
    Args:
        stacked_convs (int): number of stacked_convs
        feat_in (int): input channels of feat
        feat_out (int): output channels of feat
        num_classes (int): num_classes
        anchor_strides (list): stride of anchors
        anchor_scales (list): scale of anchors
        anchor_ratios (list): ratios of anchors
        target_means (list): target_means
        target_stds (list): target_stds
        align_conv_type (str): align_conv_type ['Conv', 'AlignConv']
        align_conv_size (int): kernel size of align_conv
        use_sigmoid_cls (bool): use sigmoid_cls or not
        reg_loss_weight (list): loss weight for regression
    """
    __shared__ = ['num_classes']
    __inject__ = ['anchor_assign']

    def __init__(self,
                 stacked_convs=2,
                 feat_in=256,
                 feat_out=256,
                 num_classes=15,
                 anchor_strides=[8, 16, 32, 64, 128],
                 anchor_scales=[4],
                 anchor_ratios=[1.0],
                 target_means=0.0,
                 target_stds=1.0,
                 align_conv_type='AlignConv',
                 align_conv_size=3,
                 use_sigmoid_cls=True,
                 anchor_assign=_get_class_default_kwargs(RBoxAssigner),
                 reg_loss_weight=[1.0, 1.0, 1.0, 1.0, 1.1],
                 cls_loss_weight=[1.1, 1.05],
                 reg_loss_type='l1'):
        super(S2ANetHead, self).__init__()
        self.stacked_convs = stacked_convs
        self.feat_in = feat_in
        self.feat_out = feat_out
        self.anchor_list = None
        self.anchor_scales = anchor_scales
        self.anchor_ratios = anchor_ratios
        self.anchor_strides = anchor_strides
        self.anchor_strides = paddle.to_tensor(anchor_strides)
        self.anchor_base_sizes = list(anchor_strides)
        self.means = paddle.ones(shape=[5]) * target_means
        self.stds = paddle.ones(shape=[5]) * target_stds
        assert align_conv_type in ['AlignConv', 'Conv', 'DCN']
        self.align_conv_type = align_conv_type
        self.align_conv_size = align_conv_size

        self.use_sigmoid_cls = use_sigmoid_cls
        self.cls_out_channels = num_classes if self.use_sigmoid_cls else 1
        self.sampling = False
        self.anchor_assign = anchor_assign
        self.reg_loss_weight = reg_loss_weight
        self.cls_loss_weight = cls_loss_weight
        self.alpha = 1.0
        self.beta = 1.0
        self.reg_loss_type = reg_loss_type
        self.s2anet_head_out = None

        # anchor
        self.anchor_generators = []
        for anchor_base in self.anchor_base_sizes:
            self.anchor_generators.append(
                S2ANetAnchorGenerator(anchor_base, anchor_scales,
                                      anchor_ratios))

        self.anchor_generators = nn.LayerList(self.anchor_generators)
        self.fam_cls_convs = nn.Sequential()
        self.fam_reg_convs = nn.Sequential()

        for i in range(self.stacked_convs):
            chan_in = self.feat_in if i == 0 else self.feat_out

            self.fam_cls_convs.add_sublayer(
                'fam_cls_conv_{}'.format(i),
                nn.Conv2D(
                    in_channels=chan_in,
                    out_channels=self.feat_out,
                    kernel_size=3,
                    padding=1,
                    weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                    bias_attr=ParamAttr(initializer=Constant(0))))

            self.fam_cls_convs.add_sublayer('fam_cls_conv_{}_act'.format(i),
                                            nn.ReLU())

            self.fam_reg_convs.add_sublayer(
                'fam_reg_conv_{}'.format(i),
                nn.Conv2D(
                    in_channels=chan_in,
                    out_channels=self.feat_out,
                    kernel_size=3,
                    padding=1,
                    weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                    bias_attr=ParamAttr(initializer=Constant(0))))

            self.fam_reg_convs.add_sublayer('fam_reg_conv_{}_act'.format(i),
                                            nn.ReLU())

        self.fam_reg = nn.Conv2D(
            self.feat_out,
            5,
            1,
            weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
            bias_attr=ParamAttr(initializer=Constant(0)))
        prior_prob = 0.01
        bias_init = float(-np.log((1 - prior_prob) / prior_prob))
        self.fam_cls = nn.Conv2D(
            self.feat_out,
            self.cls_out_channels,
            1,
            weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
            bias_attr=ParamAttr(initializer=Constant(bias_init)))

        if self.align_conv_type == "AlignConv":
            self.align_conv = AlignConv(self.feat_out, self.feat_out,
                                        self.align_conv_size)
        elif self.align_conv_type == "Conv":
            self.align_conv = nn.Conv2D(
                self.feat_out,
                self.feat_out,
                self.align_conv_size,
                padding=(self.align_conv_size - 1) // 2,
                bias_attr=ParamAttr(initializer=Constant(0)))

        elif self.align_conv_type == "DCN":
            self.align_conv_offset = nn.Conv2D(
                self.feat_out,
                2 * self.align_conv_size**2,
                1,
                weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                bias_attr=ParamAttr(initializer=Constant(0)))

            self.align_conv = paddle.vision.ops.DeformConv2D(
                self.feat_out,
                self.feat_out,
                self.align_conv_size,
                padding=(self.align_conv_size - 1) // 2,
                weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                bias_attr=False)

        self.or_conv = nn.Conv2D(
            self.feat_out,
            self.feat_out,
            kernel_size=3,
            padding=1,
            weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
            bias_attr=ParamAttr(initializer=Constant(0)))

        # ODM
        self.odm_cls_convs = nn.Sequential()
        self.odm_reg_convs = nn.Sequential()

        for i in range(self.stacked_convs):
            ch_in = self.feat_out
            # ch_in = int(self.feat_out / 8) if i == 0 else self.feat_out

            self.odm_cls_convs.add_sublayer(
                'odm_cls_conv_{}'.format(i),
                nn.Conv2D(
                    in_channels=ch_in,
                    out_channels=self.feat_out,
                    kernel_size=3,
                    stride=1,
                    padding=1,
                    weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                    bias_attr=ParamAttr(initializer=Constant(0))))

            self.odm_cls_convs.add_sublayer('odm_cls_conv_{}_act'.format(i),
                                            nn.ReLU())

            self.odm_reg_convs.add_sublayer(
                'odm_reg_conv_{}'.format(i),
                nn.Conv2D(
                    in_channels=self.feat_out,
                    out_channels=self.feat_out,
                    kernel_size=3,
                    stride=1,
                    padding=1,
                    weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
                    bias_attr=ParamAttr(initializer=Constant(0))))

            self.odm_reg_convs.add_sublayer('odm_reg_conv_{}_act'.format(i),
                                            nn.ReLU())

        self.odm_cls = nn.Conv2D(
            self.feat_out,
            self.cls_out_channels,
            3,
            padding=1,
            weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
            bias_attr=ParamAttr(initializer=Constant(bias_init)))
        self.odm_reg = nn.Conv2D(
            self.feat_out,
            5,
            3,
            padding=1,
            weight_attr=ParamAttr(initializer=Normal(0.0, 0.01)),
            bias_attr=ParamAttr(initializer=Constant(0)))

        self.featmap_sizes = []
        self.base_anchors_list = []
        self.refine_anchor_list = []

    def forward(self, feats):
        fam_reg_branch_list = []
        fam_cls_branch_list = []

        odm_reg_branch_list = []
        odm_cls_branch_list = []

        self.featmap_sizes_list = []
        self.base_anchors_list = []
        self.refine_anchor_list = []

        for feat_idx in range(len(feats)):
            feat = feats[feat_idx]
            fam_cls_feat = self.fam_cls_convs(feat)

            fam_cls = self.fam_cls(fam_cls_feat)
            # [N, CLS, H, W] --> [N, H, W, CLS]
            fam_cls = fam_cls.transpose([0, 2, 3, 1])
            fam_cls_reshape = paddle.reshape(
                fam_cls, [fam_cls.shape[0], -1, self.cls_out_channels])
            fam_cls_branch_list.append(fam_cls_reshape)

            fam_reg_feat = self.fam_reg_convs(feat)

            fam_reg = self.fam_reg(fam_reg_feat)
            # [N, 5, H, W] --> [N, H, W, 5]
            fam_reg = fam_reg.transpose([0, 2, 3, 1])
            fam_reg_reshape = paddle.reshape(fam_reg,
                                             [fam_reg.shape[0], -1, 5])
            fam_reg_branch_list.append(fam_reg_reshape)

            # prepare anchor
            featmap_size = (paddle.shape(feat)[2], paddle.shape(feat)[3])
            self.featmap_sizes_list.append(featmap_size)
            init_anchors = self.anchor_generators[feat_idx](
                featmap_size, self.anchor_strides[feat_idx])

            init_anchors = paddle.to_tensor(init_anchors, dtype='float32')
            NA = featmap_size[0] * featmap_size[1]
            init_anchors = paddle.reshape(init_anchors, [NA, 4])
            init_anchors = self.rect2rbox(init_anchors)
            self.base_anchors_list.append(init_anchors)

            if self.training:
                refine_anchor = self.bbox_decode(fam_reg.detach(),
                                                 init_anchors)
            else:
                refine_anchor = self.bbox_decode(fam_reg, init_anchors)

            self.refine_anchor_list.append(refine_anchor)

            if self.align_conv_type == 'AlignConv':
                align_feat = self.align_conv(feat,
                                             refine_anchor.clone(),
                                             featmap_size,
                                             self.anchor_strides[feat_idx])
            elif self.align_conv_type == 'DCN':
                align_offset = self.align_conv_offset(feat)
                align_feat = self.align_conv(feat, align_offset)
            elif self.align_conv_type == 'Conv':
                align_feat = self.align_conv(feat)

            or_feat = self.or_conv(align_feat)
            odm_reg_feat = or_feat
            odm_cls_feat = or_feat

            odm_reg_feat = self.odm_reg_convs(odm_reg_feat)
            odm_cls_feat = self.odm_cls_convs(odm_cls_feat)

            odm_cls_score = self.odm_cls(odm_cls_feat)
            # [N, CLS, H, W] --> [N, H, W, CLS]
            odm_cls_score = odm_cls_score.transpose([0, 2, 3, 1])
            odm_cls_score_shape = odm_cls_score.shape
            odm_cls_score_reshape = paddle.reshape(odm_cls_score, [
                odm_cls_score_shape[0], odm_cls_score_shape[1] *
                odm_cls_score_shape[2], self.cls_out_channels
            ])

            odm_cls_branch_list.append(odm_cls_score_reshape)

            odm_bbox_pred = self.odm_reg(odm_reg_feat)
            # [N, 5, H, W] --> [N, H, W, 5]
            odm_bbox_pred = odm_bbox_pred.transpose([0, 2, 3, 1])
            odm_bbox_pred_reshape = paddle.reshape(odm_bbox_pred, [-1, 5])
            odm_bbox_pred_reshape = paddle.unsqueeze(
                odm_bbox_pred_reshape, axis=0)
            odm_reg_branch_list.append(odm_bbox_pred_reshape)

        self.s2anet_head_out = (fam_cls_branch_list, fam_reg_branch_list,
                                odm_cls_branch_list, odm_reg_branch_list)
        return self.s2anet_head_out

    def get_prediction(self, nms_pre=2000):
        refine_anchors = self.refine_anchor_list
        fam_cls_branch_list = self.s2anet_head_out[0]
        fam_reg_branch_list = self.s2anet_head_out[1]
        odm_cls_branch_list = self.s2anet_head_out[2]
        odm_reg_branch_list = self.s2anet_head_out[3]
        pred_scores, pred_bboxes = self.get_bboxes(
            odm_cls_branch_list, odm_reg_branch_list, refine_anchors, nms_pre,
            self.cls_out_channels, self.use_sigmoid_cls)
        return pred_scores, pred_bboxes

    def smooth_l1_loss(self, pred, label, delta=1.0 / 9.0):
        """
        Args:
            pred: pred score
            label: label
            delta: delta
        Returns: loss
        """
        assert pred.shape == label.shape and label.numel() > 0
        assert delta > 0
        diff = paddle.abs(pred - label)
        loss = paddle.where(diff < delta, 0.5 * diff * diff / delta,
                            diff - 0.5 * delta)
        return loss

    def get_fam_loss(self, fam_target, s2anet_head_out, reg_loss_type='gwd'):
        (labels, label_weights, bbox_targets, bbox_weights, bbox_gt_bboxes,
         pos_inds, neg_inds) = fam_target
        fam_cls_branch_list, fam_reg_branch_list, odm_cls_branch_list, odm_reg_branch_list = s2anet_head_out

        fam_cls_losses = []
        fam_bbox_losses = []
        st_idx = 0
        num_total_samples = len(pos_inds) + len(
            neg_inds) if self.sampling else len(pos_inds)
        num_total_samples = max(1, num_total_samples)

        for idx, feat_size in enumerate(self.featmap_sizes_list):
            feat_anchor_num = feat_size[0] * feat_size[1]

            # step1:  get data
            feat_labels = labels[st_idx:st_idx + feat_anchor_num]
            feat_label_weights = label_weights[st_idx:st_idx + feat_anchor_num]

            feat_bbox_targets = bbox_targets[st_idx:st_idx +
                                             feat_anchor_num, :]
            feat_bbox_weights = bbox_weights[st_idx:st_idx +
                                             feat_anchor_num, :]

            # step2: calc cls loss
            feat_labels = feat_labels.reshape(-1)
            feat_label_weights = feat_label_weights.reshape(-1)

            fam_cls_score = fam_cls_branch_list[idx]
            fam_cls_score = paddle.squeeze(fam_cls_score, axis=0)
            fam_cls_score1 = fam_cls_score

            feat_labels = paddle.to_tensor(feat_labels)
            feat_labels_one_hot = paddle.nn.functional.one_hot(
                feat_labels, self.cls_out_channels + 1)
            feat_labels_one_hot = feat_labels_one_hot[:, 1:]
            feat_labels_one_hot.stop_gradient = True

            num_total_samples = paddle.to_tensor(
                num_total_samples, dtype='float32', stop_gradient=True)

            fam_cls = F.sigmoid_focal_loss(
                fam_cls_score1,
                feat_labels_one_hot,
                normalizer=num_total_samples,
                reduction='none')

            feat_label_weights = feat_label_weights.reshape(
                feat_label_weights.shape[0], 1)
            feat_label_weights = np.repeat(
                feat_label_weights, self.cls_out_channels, axis=1)
            feat_label_weights = paddle.to_tensor(
                feat_label_weights, stop_gradient=True)

            fam_cls = fam_cls * feat_label_weights
            fam_cls_total = paddle.sum(fam_cls)
            fam_cls_losses.append(fam_cls_total)

            # step3: regression loss
            feat_bbox_targets = paddle.to_tensor(
                feat_bbox_targets, dtype='float32', stop_gradient=True)
            feat_bbox_targets = paddle.reshape(feat_bbox_targets, [-1, 5])

            fam_bbox_pred = fam_reg_branch_list[idx]
            fam_bbox_pred = paddle.squeeze(fam_bbox_pred, axis=0)
            fam_bbox_pred = paddle.reshape(fam_bbox_pred, [-1, 5])
            fam_bbox = self.smooth_l1_loss(fam_bbox_pred, feat_bbox_targets)
            loss_weight = paddle.to_tensor(
                self.reg_loss_weight, dtype='float32', stop_gradient=True)
            fam_bbox = paddle.multiply(fam_bbox, loss_weight)
            feat_bbox_weights = paddle.to_tensor(
                feat_bbox_weights, stop_gradient=True)

            if reg_loss_type == 'l1':
                fam_bbox = fam_bbox * feat_bbox_weights
                fam_bbox_total = paddle.sum(fam_bbox) / num_total_samples
            elif reg_loss_type == 'iou' or reg_loss_type == 'gwd':
                fam_bbox = paddle.sum(fam_bbox, axis=-1)
                feat_bbox_weights = paddle.sum(feat_bbox_weights, axis=-1)
                try:
                    from rbox_iou_ops import rbox_iou
                except Exception as e:
                    print("import custom_ops error, try install rbox_iou_ops " \
                          "following ppdet/ext_op/README.md", e)
                    sys.stdout.flush()
                    sys.exit(-1)
                # calc iou
                fam_bbox_decode = self.delta2rbox(self.base_anchors_list[idx],
                                                  fam_bbox_pred)
                bbox_gt_bboxes = paddle.to_tensor(
                    bbox_gt_bboxes,
                    dtype=fam_bbox_decode.dtype,
                    place=fam_bbox_decode.place)
                bbox_gt_bboxes.stop_gradient = True
                iou = rbox_iou(fam_bbox_decode, bbox_gt_bboxes)
                iou = paddle.diag(iou)

                if reg_loss_type == 'gwd':
                    bbox_gt_bboxes_level = bbox_gt_bboxes[st_idx:st_idx +
                                                          feat_anchor_num, :]
                    fam_bbox_total = self.gwd_loss(fam_bbox_decode,
                                                   bbox_gt_bboxes_level)
                    fam_bbox_total = fam_bbox_total * feat_bbox_weights
                    fam_bbox_total = paddle.sum(
                        fam_bbox_total) / num_total_samples

            fam_bbox_losses.append(fam_bbox_total)
            st_idx += feat_anchor_num

        fam_cls_loss = paddle.add_n(fam_cls_losses)
        fam_cls_loss_weight = paddle.to_tensor(
            self.cls_loss_weight[0], dtype='float32', stop_gradient=True)
        fam_cls_loss = fam_cls_loss * fam_cls_loss_weight
        fam_reg_loss = paddle.add_n(fam_bbox_losses)
        return fam_cls_loss, fam_reg_loss

    def get_odm_loss(self, odm_target, s2anet_head_out, reg_loss_type='gwd'):
        (labels, label_weights, bbox_targets, bbox_weights, bbox_gt_bboxes,
         pos_inds, neg_inds) = odm_target
        fam_cls_branch_list, fam_reg_branch_list, odm_cls_branch_list, odm_reg_branch_list = s2anet_head_out

        odm_cls_losses = []
        odm_bbox_losses = []
        st_idx = 0
        num_total_samples = len(pos_inds) + len(
            neg_inds) if self.sampling else len(pos_inds)
        num_total_samples = max(1, num_total_samples)

        for idx, feat_size in enumerate(self.featmap_sizes_list):
            feat_anchor_num = feat_size[0] * feat_size[1]

            # step1:  get data
            feat_labels = labels[st_idx:st_idx + feat_anchor_num]
            feat_label_weights = label_weights[st_idx:st_idx + feat_anchor_num]

            feat_bbox_targets = bbox_targets[st_idx:st_idx +
                                             feat_anchor_num, :]
            feat_bbox_weights = bbox_weights[st_idx:st_idx +
                                             feat_anchor_num, :]

            # step2: calc cls loss
            feat_labels = feat_labels.reshape(-1)
            feat_label_weights = feat_label_weights.reshape(-1)

            odm_cls_score = odm_cls_branch_list[idx]
            odm_cls_score = paddle.squeeze(odm_cls_score, axis=0)
            odm_cls_score1 = odm_cls_score

            feat_labels = paddle.to_tensor(feat_labels)
            feat_labels_one_hot = paddle.nn.functional.one_hot(
                feat_labels, self.cls_out_channels + 1)
            feat_labels_one_hot = feat_labels_one_hot[:, 1:]
            feat_labels_one_hot.stop_gradient = True

            num_total_samples = paddle.to_tensor(
                num_total_samples, dtype='float32', stop_gradient=True)
            odm_cls = F.sigmoid_focal_loss(
                odm_cls_score1,
                feat_labels_one_hot,
                normalizer=num_total_samples,
                reduction='none')

            feat_label_weights = feat_label_weights.reshape(
                feat_label_weights.shape[0], 1)
            feat_label_weights = np.repeat(
                feat_label_weights, self.cls_out_channels, axis=1)
            feat_label_weights = paddle.to_tensor(feat_label_weights)
            feat_label_weights.stop_gradient = True

            odm_cls = odm_cls * feat_label_weights
            odm_cls_total = paddle.sum(odm_cls)
            odm_cls_losses.append(odm_cls_total)

            # # step3: regression loss
            feat_bbox_targets = paddle.to_tensor(
                feat_bbox_targets, dtype='float32')
            feat_bbox_targets = paddle.reshape(feat_bbox_targets, [-1, 5])
            feat_bbox_targets.stop_gradient = True

            odm_bbox_pred = odm_reg_branch_list[idx]
            odm_bbox_pred = paddle.squeeze(odm_bbox_pred, axis=0)
            odm_bbox_pred = paddle.reshape(odm_bbox_pred, [-1, 5])
            odm_bbox = self.smooth_l1_loss(odm_bbox_pred, feat_bbox_targets)

            loss_weight = paddle.to_tensor(
                self.reg_loss_weight, dtype='float32', stop_gradient=True)
            odm_bbox = paddle.multiply(odm_bbox, loss_weight)
            feat_bbox_weights = paddle.to_tensor(
                feat_bbox_weights, stop_gradient=True)

            if reg_loss_type == 'l1':
                odm_bbox = odm_bbox * feat_bbox_weights
                odm_bbox_total = paddle.sum(odm_bbox) / num_total_samples
            elif reg_loss_type == 'iou' or reg_loss_type == 'gwd':
                odm_bbox = paddle.sum(odm_bbox, axis=-1)
                feat_bbox_weights = paddle.sum(feat_bbox_weights, axis=-1)
                try:
                    from rbox_iou_ops import rbox_iou
                except Exception as e:
                    print("import custom_ops error, try install rbox_iou_ops " \
                          "following ppdet/ext_op/README.md", e)
                    sys.stdout.flush()
                    sys.exit(-1)
                # calc iou
                odm_bbox_decode = self.delta2rbox(self.refine_anchor_list[idx],
                                                  odm_bbox_pred)
                bbox_gt_bboxes = paddle.to_tensor(
                    bbox_gt_bboxes,
                    dtype=odm_bbox_decode.dtype,
                    place=odm_bbox_decode.place)
                bbox_gt_bboxes.stop_gradient = True
                iou = rbox_iou(odm_bbox_decode, bbox_gt_bboxes)
                iou = paddle.diag(iou)

                if reg_loss_type == 'gwd':
                    bbox_gt_bboxes_level = bbox_gt_bboxes[st_idx:st_idx +
                                                          feat_anchor_num, :]
                    odm_bbox_total = self.gwd_loss(odm_bbox_decode,
                                                   bbox_gt_bboxes_level)
                    odm_bbox_total = odm_bbox_total * feat_bbox_weights
                    odm_bbox_total = paddle.sum(
                        odm_bbox_total) / num_total_samples

            odm_bbox_losses.append(odm_bbox_total)
            st_idx += feat_anchor_num

        odm_cls_loss = paddle.add_n(odm_cls_losses)
        odm_cls_loss_weight = paddle.to_tensor(
            self.cls_loss_weight[1], dtype='float32', stop_gradient=True)
        odm_cls_loss = odm_cls_loss * odm_cls_loss_weight
        odm_reg_loss = paddle.add_n(odm_bbox_losses)
        return odm_cls_loss, odm_reg_loss

    def get_loss(self, inputs):
        # inputs: im_id image im_shape scale_factor gt_bbox gt_class is_crowd

        # compute loss
        fam_cls_loss_lst = []
        fam_reg_loss_lst = []
        odm_cls_loss_lst = []
        odm_reg_loss_lst = []

        im_shape = inputs['im_shape']
        for im_id in range(im_shape.shape[0]):
            np_im_shape = inputs['im_shape'][im_id].numpy()
            np_scale_factor = inputs['scale_factor'][im_id].numpy()
            # data_format: (xc, yc, w, h, theta)
            gt_bboxes = inputs['gt_rbox'][im_id].numpy()
            gt_labels = inputs['gt_class'][im_id].numpy()
            is_crowd = inputs['is_crowd'][im_id].numpy()
            gt_labels = gt_labels + 1

            # featmap_sizes
            anchors_list_all = np.concatenate(self.base_anchors_list)

            # get im_feat
            fam_cls_feats_list = [e[im_id] for e in self.s2anet_head_out[0]]
            fam_reg_feats_list = [e[im_id] for e in self.s2anet_head_out[1]]
            odm_cls_feats_list = [e[im_id] for e in self.s2anet_head_out[2]]
            odm_reg_feats_list = [e[im_id] for e in self.s2anet_head_out[3]]
            im_s2anet_head_out = (fam_cls_feats_list, fam_reg_feats_list,
                                  odm_cls_feats_list, odm_reg_feats_list)

            # FAM
            im_fam_target = self.anchor_assign(anchors_list_all, gt_bboxes,
                                               gt_labels, is_crowd)
            if im_fam_target is not None:
                im_fam_cls_loss, im_fam_reg_loss = self.get_fam_loss(
                    im_fam_target, im_s2anet_head_out, self.reg_loss_type)
                fam_cls_loss_lst.append(im_fam_cls_loss)
                fam_reg_loss_lst.append(im_fam_reg_loss)

            # ODM
            np_refine_anchors_list = paddle.concat(
                self.refine_anchor_list).numpy()
            np_refine_anchors_list = np.concatenate(np_refine_anchors_list)
            np_refine_anchors_list = np_refine_anchors_list.reshape(-1, 5)
            im_odm_target = self.anchor_assign(np_refine_anchors_list,
                                               gt_bboxes, gt_labels, is_crowd)

            if im_odm_target is not None:
                im_odm_cls_loss, im_odm_reg_loss = self.get_odm_loss(
                    im_odm_target, im_s2anet_head_out, self.reg_loss_type)
                odm_cls_loss_lst.append(im_odm_cls_loss)
                odm_reg_loss_lst.append(im_odm_reg_loss)
        fam_cls_loss = paddle.add_n(fam_cls_loss_lst)
        fam_reg_loss = paddle.add_n(fam_reg_loss_lst)
        odm_cls_loss = paddle.add_n(odm_cls_loss_lst)
        odm_reg_loss = paddle.add_n(odm_reg_loss_lst)
        return {
            'fam_cls_loss': fam_cls_loss,
            'fam_reg_loss': fam_reg_loss,
            'odm_cls_loss': odm_cls_loss,
            'odm_reg_loss': odm_reg_loss
        }

    def get_bboxes(self, cls_score_list, bbox_pred_list, mlvl_anchors, nms_pre,
                   cls_out_channels, use_sigmoid_cls):
        assert len(cls_score_list) == len(bbox_pred_list) == len(mlvl_anchors)

        mlvl_bboxes = []
        mlvl_scores = []

        idx = 0
        for cls_score, bbox_pred, anchors in zip(cls_score_list,
                                                 bbox_pred_list, mlvl_anchors):
            cls_score = paddle.reshape(cls_score, [-1, cls_out_channels])
            if use_sigmoid_cls:
                scores = F.sigmoid(cls_score)
            else:
                scores = F.softmax(cls_score, axis=-1)

            # bbox_pred = bbox_pred.permute(1, 2, 0).reshape(-1, 5)
            bbox_pred = paddle.transpose(bbox_pred, [1, 2, 0])
            bbox_pred = paddle.reshape(bbox_pred, [-1, 5])
            anchors = paddle.reshape(anchors, [-1, 5])

            if scores.shape[0] > nms_pre:
                # Get maximum scores for foreground classes.
                if use_sigmoid_cls:
                    max_scores = paddle.max(scores, axis=1)
                else:
                    max_scores = paddle.max(scores[:, 1:], axis=1)

                topk_val, topk_inds = paddle.topk(max_scores, nms_pre)
                anchors = paddle.gather(anchors, topk_inds)
                bbox_pred = paddle.gather(bbox_pred, topk_inds)
                scores = paddle.gather(scores, topk_inds)

            bbox_delta = paddle.reshape(bbox_pred, [-1, 5])
            bboxes = self.delta2rbox(anchors, bbox_delta)
            mlvl_bboxes.append(bboxes)
            mlvl_scores.append(scores)

            idx += 1

        mlvl_bboxes = paddle.concat(mlvl_bboxes, axis=0)
        mlvl_scores = paddle.concat(mlvl_scores)

        return mlvl_scores, mlvl_bboxes

    def rect2rbox(self, bboxes):
        """
        :param bboxes: shape (n, 4) (xmin, ymin, xmax, ymax)
        :return: dbboxes: shape (n, 5) (x_ctr, y_ctr, w, h, angle)
        """
        bboxes = paddle.reshape(bboxes, [-1, 4])
        num_boxes = paddle.shape(bboxes)[0]
        x_ctr = (bboxes[:, 2] + bboxes[:, 0]) / 2.0
        y_ctr = (bboxes[:, 3] + bboxes[:, 1]) / 2.0
        edges1 = paddle.abs(bboxes[:, 2] - bboxes[:, 0])
        edges2 = paddle.abs(bboxes[:, 3] - bboxes[:, 1])

        rbox_w = paddle.maximum(edges1, edges2)
        rbox_h = paddle.minimum(edges1, edges2)

        # set angle
        inds = edges1 < edges2
        inds = paddle.cast(inds, 'int32')
        rboxes_angle = inds * np.pi / 2.0

        rboxes = paddle.stack(
            (x_ctr, y_ctr, rbox_w, rbox_h, rboxes_angle), axis=1)
        return rboxes

    # deltas to rbox
    def delta2rbox(self, rrois, deltas, wh_ratio_clip=1e-6):
        """
        :param rrois: (cx, cy, w, h, theta)
        :param deltas: (dx, dy, dw, dh, dtheta)
        :param means: means of anchor
        :param stds: stds of anchor
        :param wh_ratio_clip: clip threshold of wh_ratio
        :return:
        """
        deltas = paddle.reshape(deltas, [-1, 5])
        rrois = paddle.reshape(rrois, [-1, 5])
        # fix dy2st bug denorm_deltas = deltas * self.stds + self.means
        denorm_deltas = paddle.add(
            paddle.multiply(deltas, self.stds), self.means)

        dx = denorm_deltas[:, 0]
        dy = denorm_deltas[:, 1]
        dw = denorm_deltas[:, 2]
        dh = denorm_deltas[:, 3]
        dangle = denorm_deltas[:, 4]
        max_ratio = np.abs(np.log(wh_ratio_clip))
        dw = paddle.clip(dw, min=-max_ratio, max=max_ratio)
        dh = paddle.clip(dh, min=-max_ratio, max=max_ratio)

        rroi_x = rrois[:, 0]
        rroi_y = rrois[:, 1]
        rroi_w = rrois[:, 2]
        rroi_h = rrois[:, 3]
        rroi_angle = rrois[:, 4]

        gx = dx * rroi_w * paddle.cos(rroi_angle) - dy * rroi_h * paddle.sin(
            rroi_angle) + rroi_x
        gy = dx * rroi_w * paddle.sin(rroi_angle) + dy * rroi_h * paddle.cos(
            rroi_angle) + rroi_y
        gw = rroi_w * dw.exp()
        gh = rroi_h * dh.exp()
        ga = np.pi * dangle + rroi_angle
        ga = (ga + np.pi / 4) % np.pi - np.pi / 4
        ga = paddle.to_tensor(ga)
        gw = paddle.to_tensor(gw, dtype='float32')
        gh = paddle.to_tensor(gh, dtype='float32')
        bboxes = paddle.stack([gx, gy, gw, gh, ga], axis=-1)
        return bboxes

    def bbox_decode(self, bbox_preds, anchors):
        """decode bbox from deltas
        Args:
            bbox_preds: [N,H,W,5]
            anchors: [H*W,5]
        return:
            bboxes: [N,H,W,5]
        """
        num_imgs, H, W, _ = bbox_preds.shape
        bbox_delta = paddle.reshape(bbox_preds, [-1, 5])
        bboxes = self.delta2rbox(anchors, bbox_delta)
        return bboxes

    def trace(self, A):
        tr = paddle.diagonal(A, axis1=-2, axis2=-1)
        tr = paddle.sum(tr, axis=-1)
        return tr

    def sqrt_newton_schulz_autograd(self, A, numIters):
        A_shape = A.shape
        batchSize = A_shape[0]
        dim = A_shape[1]

        normA = A * A
        normA = paddle.sum(normA, axis=1)
        normA = paddle.sum(normA, axis=1)
        normA = paddle.sqrt(normA)
        normA1 = normA.reshape([batchSize, 1, 1])
        Y = paddle.divide(A, paddle.expand_as(normA1, A))
        I = paddle.eye(dim, dim).reshape([1, dim, dim])
        l0 = []
        for i in range(batchSize):
            l0.append(I)
        I = paddle.concat(l0, axis=0)
        I.stop_gradient = False
        Z = paddle.eye(dim, dim).reshape([1, dim, dim])
        l1 = []
        for i in range(batchSize):
            l1.append(Z)
        Z = paddle.concat(l1, axis=0)
        Z.stop_gradient = False

        for i in range(numIters):
            T = 0.5 * (3.0 * I - Z.bmm(Y))
            Y = Y.bmm(T)
            Z = T.bmm(Z)
        sA = Y * paddle.sqrt(normA1).reshape([batchSize, 1, 1])
        sA = paddle.expand_as(sA, A)
        return sA

    def wasserstein_distance_sigma(sigma1, sigma2):
        wasserstein_distance_item2 = paddle.matmul(
            sigma1, sigma1) + paddle.matmul(
                sigma2, sigma2) - 2 * self.sqrt_newton_schulz_autograd(
                    paddle.matmul(
                        paddle.matmul(sigma1, paddle.matmul(sigma2, sigma2)),
                        sigma1), 10)
        wasserstein_distance_item2 = self.trace(wasserstein_distance_item2)

        return wasserstein_distance_item2

    def xywhr2xyrs(self, xywhr):
        xywhr = paddle.reshape(xywhr, [-1, 5])
        xy = xywhr[:, :2]
        wh = paddle.clip(xywhr[:, 2:4], min=1e-7, max=1e7)
        r = xywhr[:, 4]
        cos_r = paddle.cos(r)
        sin_r = paddle.sin(r)
        R = paddle.stack(
            (cos_r, -sin_r, sin_r, cos_r), axis=-1).reshape([-1, 2, 2])
        S = 0.5 * paddle.nn.functional.diag_embed(wh)
        return xy, R, S

    def gwd_loss(self,
                 pred,
                 target,
                 fun='log',
                 tau=1.0,
                 alpha=1.0,
                 normalize=False):

        xy_p, R_p, S_p = self.xywhr2xyrs(pred)
        xy_t, R_t, S_t = self.xywhr2xyrs(target)

        xy_distance = (xy_p - xy_t).square().sum(axis=-1)

        Sigma_p = R_p.matmul(S_p.square()).matmul(R_p.transpose([0, 2, 1]))
        Sigma_t = R_t.matmul(S_t.square()).matmul(R_t.transpose([0, 2, 1]))

        whr_distance = paddle.diagonal(
            S_p, axis1=-2, axis2=-1).square().sum(axis=-1)

        whr_distance = whr_distance + paddle.diagonal(
            S_t, axis1=-2, axis2=-1).square().sum(axis=-1)
        _t = Sigma_p.matmul(Sigma_t)

        _t_tr = paddle.diagonal(_t, axis1=-2, axis2=-1).sum(axis=-1)
        _t_det_sqrt = paddle.diagonal(S_p, axis1=-2, axis2=-1).prod(axis=-1)
        _t_det_sqrt = _t_det_sqrt * paddle.diagonal(
            S_t, axis1=-2, axis2=-1).prod(axis=-1)
        whr_distance = whr_distance + (-2) * (
            (_t_tr + 2 * _t_det_sqrt).clip(0).sqrt())

        distance = (xy_distance + alpha * alpha * whr_distance).clip(0)

        if normalize:
            wh_p = pred[..., 2:4].clip(min=1e-7, max=1e7)
            wh_t = target[..., 2:4].clip(min=1e-7, max=1e7)
            scale = ((wh_p.log() + wh_t.log()).sum(dim=-1) / 4).exp()
            distance = distance / scale

        if fun == 'log':
            distance = paddle.log1p(distance)

        if tau >= 1.0:
            return 1 - 1 / (tau + distance)

        return distance


import requests

url = "https://dapi.kakao.com/v2/search/cafe"   #찾을 사이트- 검색 영역
header = {'Authorization' : 'KakaoAK ' + "e68603d86178518e0161a32013b7fe54"}   #api키
queryString = {'query': "아이유"}    # 검색 키워드
#                                     키 : 키값              권한
r = requests.get(url, headers=header, params=queryString)

for i in r.json()["documents"] :
    print (i["title"] )
    print (i["url"])
    print (i["datetime"])
    print (i["contents"])


""" This module provides access the Tesla Motors Owner API. It uses Tesla's new
RFC compliant OAuth 2 Single Sign-On service. Tokens are saved to 'cache.json'
for reuse and refreshed automatically. The vehicle option codes are loaded from
'option_codes.json' and the API endpoints are loaded from 'endpoints.json'.
"""

# Author: Tim Dorssers

__version__ = '2.8.0'

import os
import ast
import json
import time
import base64
import hashlib
import logging
import pkgutil
import datetime
import webbrowser
import stat
try:
    from urlparse import urljoin
except ImportError:
    from urllib.parse import urljoin
from collections import defaultdict, namedtuple
import requests
from requests_oauthlib import OAuth2Session
from requests.exceptions import *
from requests.packages.urllib3.util.retry import Retry
from oauthlib.oauth2.rfc6749.errors import *
import websocket  # websocket-client v0.49.0 up to v0.58.0 is not supported

requests.packages.urllib3.disable_warnings()

BASE_URL = 'https://owner-api.teslamotors.com/'
SSO_BASE_URL = 'https://auth.tesla.com/'
SSO_CLIENT_ID = 'ownerapi'
STREAMING_BASE_URL = 'wss://streaming.vn.teslamotors.com/'
APP_USER_AGENT = 'TeslaApp/4.10.0'

# Setup module logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

# Py2/3 compatibility
try:
    input = raw_input
except NameError:
    pass

class Tesla(OAuth2Session):
    """ Implements a session manager for the Tesla Motors Owner API

    email: SSO identity.
    verify: (optional) Verify SSL certificate.
    proxy: (optional) URL of proxy server.
    retry: (optional) Number of connection retries or `Retry` instance.
    timeout: (optional) Connect/read timeout.
    user_agent: (optional) The User-Agent string.
    authenticator: (optional) Function with one argument, the authorization URL,
                   that returns the redirected URL.
    cache_file: (optional) Path to cache file used by default loader and dumper.
    cache_loader: (optional) Function that returns the cache dict.
    cache_dumper: (optional) Function with one argument, the cache dict.
    sso_base_url: (optional) URL of SSO service, set to `https://auth.tesla.cn/`
                  if your email is registered in another region.
    code_verifier (optional): PKCE code verifier string.
    app_user_agent (optional): X-Tesla-User-Agent string.

    Extra keyword arguments to pass to OAuth2Session constructor using `kwargs`:
    state (optional): A state string for CSRF protection.
    """

    def __init__(self, email, verify=True, proxy=None, retry=0, timeout=10,
                 user_agent=__name__ + '/' + __version__, authenticator=None,
                 cache_file='cache.json', cache_loader=None, cache_dumper=None,
                 sso_base_url=None, code_verifier=None,
                 app_user_agent=APP_USER_AGENT, **kwargs):
        super(Tesla, self).__init__(client_id=SSO_CLIENT_ID, **kwargs)
        if not email:
            raise ValueError('`email` is not set')
        self.email = email
        self.authenticator = authenticator or self._authenticate
        self.cache_loader = cache_loader or self._cache_load
        self.cache_dumper = cache_dumper or self._cache_dump
        self.cache_file = cache_file
        self.timeout = timeout
        self.endpoints = {}
        self.sso_base_url = sso_base_url or SSO_BASE_URL
        self._auto_refresh_url = None
        self.code_verifier = code_verifier
        # Set OAuth2Session properties
        self.scope = ('openid', 'email', 'offline_access')
        self.redirect_uri = SSO_BASE_URL + 'void/callback'
        self.auto_refresh_url = 'oauth2/v3/token'
        self.auto_refresh_kwargs = {'client_id': SSO_CLIENT_ID}
        self.token_updater = self._token_updater
        self.mount('https://', requests.adapters.HTTPAdapter(max_retries=retry))
        self.headers.update({'Content-Type': 'application/json',
                             'X-Tesla-User-Agent': app_user_agent,
                             'User-Agent': user_agent})
        self.verify = verify
        if proxy:
            self.trust_env = False
            self.proxies.update({'https': proxy})
        self._token_updater()  # Try to read token from cache
        logger.debug('Using SSO service URL %s', self.sso_base_url)

    @property
    def expires_at(self):
        """ Returns unix time when token needs refreshing """
        return self.token.get('expires_at')

    @property
    def auto_refresh_url(self):
        """ Returns refresh token endpoint URL for auto-renewal access token """
        url = urljoin(self.sso_base_url, self._auto_refresh_url)
        return url if self._auto_refresh_url else None

    @auto_refresh_url.setter
    def auto_refresh_url(self, url):
        """ Sets refresh token endpoint URL for auto-renewal of access token """
        self._auto_refresh_url = url

    def request(self, method, url, serialize=True, **kwargs):
        """ Overriddes base method to support relative URLs, serialization and
        error message handling. Raises HTTPError when an error occurs.

        method: HTTP method to use.
        url: URL to send.
        serialize (optional): (de)serialize request/response body.

        Extra keyword arguments to pass to base method using `kwargs`:
        withhold_token (optional): perform unauthenticated request.
        params (optional): URL parameters to append to the URL.
        data (optional): the body to attach to the request.
        json (optional): json for the body to attach to the request.

        Return type: JsonDict or String or requests.Response
        """
        if url.startswith(self.sso_base_url):
            return super(Tesla, self).request(method, url, **kwargs)
        # Construct URL and send request with optional serialized data
        url = urljoin(BASE_URL, url)
        kwargs.setdefault('timeout', self.timeout)
        if serialize and 'data' in kwargs:
            kwargs['json'] = kwargs.pop('data')
        response = super(Tesla, self).request(method, url, **kwargs)
        # Error message handling
        if serialize and 400 <= response.status_code < 600:
            try:
                lst = [str(v).strip('.') for v in response.json().values() if v]
                response.reason = '. '.join(lst)
            except ValueError:
                pass
        response.raise_for_status()  # Raise HTTPError, if one occurred
        # Deserialize response
        if serialize:
            return response.json(object_hook=JsonDict)
        return response.text

    @staticmethod
    def new_code_verifier():
        """ Generate code verifier for PKCE as per RFC 7636 section 4.1 """
        result = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=')
        logger.debug('Generated new code verifier %s.',
                     result.decode() if isinstance(result, bytes) else result)
        return result

    def authorization_url(self, url='oauth2/v3/authorize',
                          code_verifier=None, **kwargs):
        """ Overriddes base method to form an authorization URL with PKCE
        extension for Tesla's SSO service.

        url (optional): Authorization endpoint url.
        code_verifier (optional): PKCE code verifier string.

        Extra keyword arguments to pass to base method using `kwargs`:
        state (optional): A state string for CSRF protection.

        Return type: String or None
        """
        if self.authorized:
            return None
        # Generate code verifier and challenge for PKCE (RFC 7636)
        self.code_verifier = code_verifier or self.new_code_verifier()
        unencoded_digest = hashlib.sha256(self.code_verifier).digest()
        code_challenge = base64.urlsafe_b64encode(unencoded_digest).rstrip(b'=')
        # Prepare for OAuth 2 Authorization Code Grant flow
        url = urljoin(self.sso_base_url, url)
        kwargs['code_challenge'] = code_challenge
        kwargs['code_challenge_method'] = 'S256'
        without_hint, state = super(Tesla, self).authorization_url(url, **kwargs)
        # Detect account's registered region
        kwargs['login_hint'] = self.email
        kwargs['state'] = state
        with_hint = super(Tesla, self).authorization_url(url, **kwargs)[0]
        response = self.get(with_hint, allow_redirects=False)
        if response.is_redirect:
            with_hint = response.headers['Location']
            self.sso_base_url = urljoin(with_hint, '/')
            logger.debug('New SSO service URL %s', self.sso_base_url)
        return with_hint if response.ok else without_hint

    def fetch_token(self, token_url='oauth2/v3/token', **kwargs):
        """ Overriddes base method to sign into Tesla's SSO service using
        Authorization Code grant with PKCE extension. Raises CustomOAuth2Error.

        token_url (optional): Token endpoint URL.

        Extra keyword arguments to pass to base method using `kwargs`:
        authorization_response (optional): Authorization response URL.
        code_verifier (optional): Code verifier cryptographic random string.

        Return type: dict
        """
        if self.authorized:
            return self.token
        if kwargs.get('authorization_response') is None:
            # Open SSO page for user authorization through redirection
            url = self.authorization_url()
            kwargs['authorization_response'] = self.authenticator(url)
        # Use authorization code in redirected location to get token
        token_url = urljoin(self.sso_base_url, token_url)
        kwargs['include_client_id'] = True
        kwargs.setdefault('verify', self.verify)
        kwargs.setdefault('code_verifier', self.code_verifier)
        super(Tesla, self).fetch_token(token_url, **kwargs)
        self._token_updater()  # Save new token
        return self.token

    def refresh_token(self, token_url='oauth2/v3/token', **kwargs):
        """ Overriddes base method to refresh Tesla's SSO token. Raises
        ValueError and ServerError.

        token_url (optional): The token endpoint.

        Extra keyword arguments to pass to base method using `kwargs`:
        refresh_token (optional): The refresh_token to use.

        Return type: dict
        """
        if not self.authorized and not kwargs.get('refresh_token'):
            raise ValueError('`refresh_token` is not set')
        token_url = urljoin(self.sso_base_url, token_url)
        kwargs.setdefault('verify', self.verify)
        super(Tesla, self).refresh_token(token_url, **kwargs)
        self._token_updater()  # Save new token
        return self.token

    def close(self):
        """ Overriddes base method to remove all adapters on close """
        super(Tesla, self).close()
        self.adapters.clear()

    def logout(self, sign_out=False):
        """ Removes token from cache, returns logout URL, and optionally logs
        out of default browser.

        sign_out (optional): sign out using system's default web browser.

        Return type: String or None
        """
        if not self.authorized:
            return None
        url = self.sso_base_url + 'oauth2/v3/logout?client_id=' + SSO_CLIENT_ID
        # Built-in sign out method
        if sign_out:
            if webbrowser.open(url):
                logger.debug('Opened %s with default browser', url)
            else:
                print('Open this URL to sign out: ' + url)
        # Empty token dict, update cache and remove access_token
        self.token = {}
        self._token_updater()
        del self.access_token
        return url

    @staticmethod
    def _authenticate(url):
        """ Default authenticator method """
        print('Use browser to login. Page Not Found will be shown at success.')
        if webbrowser.open(url):
            logger.debug('Opened %s with default browser', url)
        else:
            print('Open this URL to authenticate: ' + url)
        return input('Enter URL after authentication: ')

    def _cache_load(self):
        """ Default cache loader method """
        try:
            with open(self.cache_file) as infile:
                cache = json.load(infile)
        except (IOError, ValueError) as e:
            logger.warning('Cannot load cache: %s',
                           self.cache_file, exc_info=True)
            cache = {}
        return cache

    def _cache_dump(self, cache):
        """ Default cache dumper method """
        try:
            with open(self.cache_file, 'w') as outfile:
                json.dump(cache, outfile)
            os.chmod(self.cache_file, (stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP))
        except IOError:
            logger.error('Cache not updated')
        else:
            logger.debug('Updated cache')

    def _token_updater(self, token=None):
        """ Handles token persistency. Raises ValueError. """
        if token:
            return  # Don't update token twice when auto refreshing
        cache = self.cache_loader()
        if not isinstance(cache, dict):
            raise ValueError('`cache_loader` must return dict')
        # Write token to cache
        if self.authorized:
            cache[self.email] = {'url': self.sso_base_url, 'sso': self.token}
            self.cache_dumper(cache)
        # Read token from cache
        elif self.email in cache:
            self.sso_base_url = cache[self.email].get('url', self.sso_base_url)
            self.token = cache[self.email].get('sso', {})
            if not self.token:
                return
            # Log the token validity
            if 0 < self.expires_at < time.time():
                logger.debug('Cached SSO token expired')
            else:
                logger.debug('Cached SSO token expires at %s',
                             time.ctime(self.expires_at))

    def api(self, name, path_vars=None, **kwargs):
        """ Convenience method to perform API request for given endpoint name,
        with keyword arguments as parameters. Substitutes path variables in URI
        using path_vars. Raises ValueError.

        Return type: JsonDict or String
        """
        path_vars = path_vars or {}
        # Load API endpoints once
        if not self.endpoints:
            try:
                data = pkgutil.get_data(__name__, 'endpoints.json')
                self.endpoints = json.loads(data.decode())
                logger.debug('%d endpoints loaded', len(self.endpoints))
            except (IOError, ValueError):
                logger.error('No endpoints loaded')
        # Lookup endpoint name
        try:
            endpoint = self.endpoints[name]
        except KeyError:
            raise ValueError('Unknown endpoint name ' + name)
        # Fetch token if not authorized and API requires authorization
        if endpoint['AUTH'] and not self.authorized:
            self.fetch_token()
        # Substitute path variables in URI
        try:
            uri = endpoint['URI'].format(**path_vars)
        except KeyError as e:
            raise ValueError('%s requires path variable %s' % (name, e))
        # Perform request using given keyword arguments as parameters
        arg_name = 'params' if endpoint['TYPE'] == 'GET' else 'json'
        serialize = endpoint.get('CONTENT') != 'HTML' and name != 'STATUS'
        return self.request(endpoint['TYPE'], uri, serialize,
                            withhold_token=not endpoint['AUTH'],
                            **{arg_name: kwargs})

    def vehicle_list(self):
        """ Returns a list of `Vehicle` objects """
        return [Vehicle(v, self) for v in self.api('VEHICLE_LIST')['response']]

    def battery_list(self):
        """ Returns a list of `Battery` objects """
        return [Battery(p, self) for p in self.api('PRODUCT_LIST')['response']
                if p.get('resource_type') == 'battery']

    def solar_list(self):
        """ Returns a list of `SolarPanel` objects """
        return [SolarPanel(p, self) for p in self.api('PRODUCT_LIST')['response']
                if p.get('resource_type') == 'solar']

class VehicleError(Exception):
    """ Vehicle exception class """
    pass

class JsonDict(dict):
    """ Pretty printing dictionary """

    def __str__(self):
        """ Serialize dict to JSON formatted string with indents """
        return json.dumps(self, indent=4)

class Vehicle(JsonDict):
    """ Vehicle class with dictionary access and API request support """

    codes = None  # Vehicle option codes class variable
    COLS = ['speed', 'odometer', 'soc', 'elevation', 'est_heading', 'est_lat',
            'est_lng', 'power', 'shift_state', 'range', 'est_range', 'heading']

    def __init__(self, vehicle, tesla):
        super(Vehicle, self).__init__(vehicle)
        self.tesla = tesla
        self.callback = None
        self.timestamp = time.time()

    def _subscribe(self, wsapp):
        """ Authenticate and select streaming telemetry columns """
        msg = {'msg_type': 'data:subscribe_oauth', 'value': ','.join(self.COLS),
               'token': self.tesla.access_token, 'tag': str(self['vehicle_id'])}
        wsapp.send(json.dumps(msg))

    def _parse_msg(self, wsapp, message):
        """ Parse messages """
        msg = json.loads(message)
        if msg['msg_type'] == 'control:hello':
            logger.debug('connected')
        elif msg['msg_type'] == 'data:update':
            # Parse comma separated data record
            data = dict(zip(['timestamp'] + self.COLS, msg['value'].split(',')))
            for key, value in data.items():
                try:
                    data[key] = ast.literal_eval(value) if value else None
                except (SyntaxError, ValueError):
                    pass
            logger.debug('Update %s', json.dumps(data))
            if self.callback:
                self.callback(data)
            # Update polled data with streaming telemetry data
            drive_state = self.setdefault('drive_state', JsonDict())
            vehicle_state = self.setdefault('vehicle_state', JsonDict())
            charge_state = self.setdefault('charge_state', JsonDict())
            drive_state['timestamp'] = data['timestamp']
            drive_state['speed'] = data['speed']
            vehicle_state['odometer'] = data['odometer']
            charge_state['battery_level'] = data['soc']
            drive_state['heading'] = data['est_heading']
            drive_state['latitude'] = data['est_lat']
            drive_state['longitude'] = data['est_lng']
            drive_state['power'] = data['power']
            drive_state['shift_state'] = data['shift_state']
            charge_state['ideal_battery_range'] = data['range']
            charge_state['est_battery_range'] = data['est_range']
            drive_state['heading'] = data['heading']
        elif msg['msg_type'] == 'data:error':
            logger.error(msg['value'])
            wsapp.close()

    @staticmethod
    def _ws_error(wsapp, err):
        """ Log exceptions """
        logger.error(err)

    def stream(self, callback=None, retry=0, indefinitely=False, **kwargs):
        """ Let vehicle push on-change data, with 10 second idle timeout.

        callback: (optional) Function with one argument, a dict of pushed data.
        retry: (optional) Number of connection retries.
        indefinitely: (optional) Retry indefinitely.
        **kwargs: Optional arguments that `run_forever` takes.
        """
        self.callback = callback
        websocket.enableTrace(logger.isEnabledFor(logging.DEBUG),
                              handler=logging.NullHandler())
        wsapp = websocket.WebSocketApp(STREAMING_BASE_URL + 'streaming/',
                                       on_open=self._subscribe,
                                       on_message=self._parse_msg,
                                       on_error=self._ws_error)
        kwargs.setdefault('ping_interval', 10)
        while True:
            wsapp.run_forever(**kwargs)
            if indefinitely:
                continue
            if not retry:
                break
            logger.debug('%d retries left', retry)
            retry -= 1

    def api(self, name, **kwargs):
        """ Endpoint request with vehicle_id path variable """
        return self.tesla.api(name, {'vehicle_id': self['id_s']}, **kwargs)

    def get_vehicle_summary(self):
        """ Determine the state of the vehicle's various sub-systems """
        self.update(self.api('VEHICLE_SUMMARY')['response'])
        self.timestamp = time.time()
        return self

    def available(self, max_age=60):
        """ Determine vehicle availability based on the cached data or the
        refreshed status when aged out. """
        if self.timestamp + max_age < time.time():
            self.get_vehicle_summary()
        return self['state'] == 'online'

    def sync_wake_up(self, timeout=60, interval=2, backoff=1.15):
        """ Wakes up vehicle if needed and waits for it to come online. Raises
        VehicleError if not woken up within timeout. """
        logger.info('%s is %s', self['display_name'], self['state'])
        if not self.available():
            self.api('WAKE_UP')  # Send wake up command
            start_time = time.time()
            while True:
                logger.debug('Waiting for %d seconds', interval)
                time.sleep(int(interval))
                if self.available(0):
                    break
                # Raise exception when task has timed out
                if start_time + timeout - interval < time.time():
                    raise VehicleError('%s not woken up within %s seconds'
                                       % (self['display_name'], timeout))
                interval *= backoff
            logger.info('%s is %s', self['display_name'], self['state'])

    @classmethod
    def decode_option(cls, code):
        """ Returns option code title or None if unknown """
        # Load option codes once
        if cls.codes is None:
            try:
                data = pkgutil.get_data(__name__, 'option_codes.json')
                cls.codes = json.loads(data.decode())
                logger.debug('%d option codes loaded', len(Vehicle.codes))
            except (IOError, ValueError):
                cls.codes = {}
                logger.error('No option codes loaded')
        # Lookup option code title
        return cls.codes.get(code)

    def option_code_list(self):
        """ Returns a list of known vehicle option code titles """
        codes = self['option_codes'] if self['option_codes'] else ''
        return list(filter(None, [self.decode_option(code)
                                  for code in codes.split(',')]))

    def get_vehicle_data(self):
        """ A rollup of all the data request endpoints plus vehicle config.
        Raises HTTPError when vehicle is not online. """
        self.update(self.api('VEHICLE_DATA')['response'])
        self.timestamp = time.time()
        return self

    def get_nearby_charging_sites(self):
        """ Lists nearby Tesla-operated charging stations. Raises HTTPError when
        vehicle is in service or not online. """
        return self.api('NEARBY_CHARGING_SITES')['response']

    def get_service_scheduling_data(self):
        """ Retrieves next service appointment for this vehicle """
        response = self.api('GET_UPCOMING_SERVICE_VISIT_DATA')['response']
        return next((enabled for enabled in response['enabled_vins']
                     if enabled['vin'] == self['vin']), {})

    def get_charge_history(self):
        """ Lists vehicle charging history data points """
        return self.api('VEHICLE_CHARGE_HISTORY')['response']

    def mobile_enabled(self):
        """ Checks if the Mobile Access setting is enabled in the car. Raises
        HTTPError when vehicle is in service or not online. """
        # Construct URL and send request
        uri = 'api/1/vehicles/%s/mobile_enabled' % self['id_s']
        return self.tesla.get(uri)['response']

    def compose_image(self, view='STUD_3QTR', size=640, options=None):
        """ Returns a PNG formatted composed vehicle image. Valid views are:
        STUD_3QTR, STUD_SEAT, STUD_SIDE, STUD_REAR and STUD_WHEEL """
        if options is None and self['option_codes'] is None:
            raise ValueError('`compose_image` requires `options` to be set')
        # Derive model from VIN and other properties from (given) option codes
        params = {'model': 'm' + self['vin'][3].lower(),
                  'bkba_opt': 1, 'view': view, 'size': size,
                  'options': options or self['option_codes']}
        # Retrieve image from compositor
        url = 'https://static-assets.tesla.com/v1/compositor/'
        response = requests.get(url, params=params, verify=self.tesla.verify,
                                proxies=self.tesla.proxies)
        response.raise_for_status()  # Raise HTTPError, if one occurred
        return response.content

    def __missing__(self, key):
        """ Get cached data when accessed. Raises KeyError on invalid key. """
        if key not in self.get_vehicle_data():
            raise KeyError(key)
        return self[key]

    def dist_units(self, miles, speed=False):
        """ Format and convert distance or speed to GUI setting units """
        if miles is None:
            return None
        # Lookup GUI settings of the vehicle
        if 'km' in self['gui_settings']['gui_distance_units']:
            return '%.1f %s' % (miles * 1.609344, 'km/h' if speed else 'km')
        return '%.1f %s' % (miles, 'mph' if speed else 'mi')

    def temp_units(self, celcius):
        """ Format and convert temperature to GUI setting units """
        if celcius is None:
            return None
        # Lookup GUI settings of the vehicle
        if 'F' in self['gui_settings']['gui_temperature_units']:
            return '%.1f F' % (celcius * 1.8 + 32)
        return '%.1f C' % celcius

    def gui_time(self, timestamp_ms=0):
        """ Returns timestamp or current time formatted to GUI setting """
        tm = time.localtime(timestamp_ms / 1000 or None)
        # Lookup GUI settings of the vehicle
        if self['gui_settings']['gui_24_hour_time']:
            return time.strftime('%H:%M:%S', tm)
        return time.strftime('%I:%M:%S %p', tm)

    def last_seen(self):
        """ Returns vehicle last seen natural time. Raises ValueError. """
        units = ((60, 'a second'), (60, 'a minute'), (24, 'an hour'),
                 (7, 'a day'), (4.35, 'a week'), (12, 'a month'), (0, 'a year'))
        diff = time.time() - self['charge_state']['timestamp'] / 1000
        if diff < 0:
            raise ValueError('Timestamp is in the future')
        if diff >= 1:
            for length, unit in units:
                if diff < length or not length:
                    if diff > 1.5:
                        unit = '%d %ss' % (round(diff), unit.split()[1])
                    return unit + ' ago'
                diff /= length
        return 'just now'

    def decode_vin(self):
        """ Returns decoded VIN as dict """
        make = 'Tesla Model ' + self['vin'][3]
        body = {'A': 'Hatch back 5 Dr / LHD', 'B': 'Hatch back 5 Dr / RHD',
                'C': 'Class E MPV / 5 Dr / LHD', 'E': 'Sedan 4 Dr / LHD',
                'D': 'Class E MPV / 5 Dr / RHD', 'F': 'Sedan 4 Dr / RHD',
                'G': 'Class D MPV / 5 Dr / LHD', 'H': 'Class D MPV / 5 Dr / RHD'
                }.get(self['vin'][4], 'Unknown')
        belt = {'1': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, '
                     'PODS, side inflatable restraints, knee airbags (FR)',
                '3': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, '
                     'side inflatable restraints, knee airbags (FR)',
                '4': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, '
                     'side inflatable restraints, knee airbags (FR)',
                '5': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, '
                     'side inflatable restraints',
                '6': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, '
                     'side inflatable restraints',
                '7': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, '
                     'side inflatable restraints & active hood',
                '8': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, '
                     'side inflatable restraints & active hood',
                'A': 'Type 2 manual seatbelts (FR, SR*3, TR*2) with front '
                     'airbags, PODS, side inflatable restraints, knee airbags (FR)',
                'B': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front '
                     'airbags, PODS, side inflatable restraints, knee airbags (FR)',
                'C': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front '
                     'airbags, PODS, side inflatable restraints, knee airbags (FR)',
                'D': 'Type 2 Manual Seatbelts (FR, SR*3) with front airbag, '
                     'PODS, side inflatable restraints, knee airbags (FR)'
                }.get(self['vin'][5], 'Unknown')
        batt = {'E': 'Electric (NMC)', 'F': 'Li-Phosphate (LFP)',
                'H': 'High Capacity (NMC)', 'S': 'Standard (NMC)',
                'V': 'Ultra Capacity (NMC)'}.get(self['vin'][6], 'Unknown')
        drive = {'1': 'Single Motor - Standard', '2': 'Dual Motor - Standard',
                 '3': 'Single Motor - Performance', '5': 'P2 Dual Motor',
                 '4': 'Dual Motor - Performance', '6': 'P2 Tri Motor',
                 'A': 'Single Motor', 'B': 'Dual Motor - Standard',
                 'C': 'Dual Motor - Performance', 'D': 'Single Motor',
                 'E': 'Dual Motor - Standard', 'F': 'Dual Motor - Performance',
                 'P': 'Performance, Tier 7', 'G': 'Base, Tier 4',
                 'N': 'Base, Tier 7'}.get(self['vin'][7], 'Unknown')
        year = 2009 + '9ABCDEFGHJKLMNPRSTVWXY12345678'.index(self['vin'][9])
        plant = {'1': 'Menlo Park, CA, USA', '3': 'Hethel, UK',
                 'B': 'Berlin, Germany', 'C': 'Shanghai, China',
                 'F': 'Fremont, CA, USA', 'P': 'Palo Alto, CA, USA',
                 'R': 'Research'}.get(self['vin'][10], 'Unknown')
        return JsonDict(manufacturer='Tesla Motors, Inc.', make=make,
                        body_type=body, belt_system=belt, battery_type=batt,
                        drive_unit=drive, year=str(year), plant_code=plant)

    def command(self, name, **kwargs):
        """ Wrapper method for vehicle command response error handling. Raises
        VehicleError or HTTPError. """
        response = self.api(name, **kwargs).get('response')
        if not response or 'result' not in response:
            raise VehicleError(name + " doesn't seem to be a command")
        if not response['result']:
            raise VehicleError(response['reason'])
        return response['result']

class ProductError(Exception):
    """ Product exception class """
    pass

class Product(JsonDict):
    """ Base product class with dictionary access and API request support """

    def __init__(self, product, tesla):
        super(Product, self).__init__(product)
        self.tesla = tesla

    def api(self, name, **kwargs):
        """ Endpoint request with battery_id or site_id path variable """
        pathvars = {'battery_id': self['id'], 'site_id': self['energy_site_id']}
        return self.tesla.api(name, pathvars, **kwargs)

    def get_calendar_history_data(
            self, kind='energy', period='day', start_date=None,
            end_date=time.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
            installation_timezone=None, timezone=None, tariff=None):
        """ Retrieve live status of product
        kind: A telemetry type of 'backup', 'energy', 'power',
              'self_consumption', 'time_of_use_energy',
              'time_of_use_self_consumption', 'savings' and 'soe'
        period: 'day', 'month', 'year', or 'lifetime'
        end_date: The final day in the data requested in the json format
                  '2021-02-28T07:59:59.999Z'
        time_zone: Timezone in the json timezone format. eg. Europe/Brussels
        start_date: The state date in the data requested in the json format
                    '2021-02-27T07:59:59.999Z'
        installation_timezone: Timezone of installation location for 'savings'
        tariff: Unclear format use in 'savings' only
        """
        return self.api('CALENDAR_HISTORY_DATA', kind=kind, period=period,
                        start_date=start_date, end_date=end_date,
                        installation_timezone=installation_timezone,
                        timezone=timezone, tariff=tariff)['response']

    def get_history_data(
            self, kind='energy', period='day', start_date=None,
            end_date=time.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
            installation_timezone=None, timezone=None):
        """ Retrieve live status of product
        kind: A telemetry type of 'backup', 'energy', 'power',
              'self_consumption', 'time_of_use_energy', and
              'time_of_use_self_consumption'
        period: 'day', 'month', 'year', or 'lifetime'
        end_date: The final day in the data requested in the json format
                  '2021-02-28T07:59:59.999Z'
        time_zone: Timezone in the json timezone format. eg. Europe/Brussels
        start_date: The state date in the data requested in the json format
                    '2021-02-27T07:59:59.999Z'
        installation_timezone: Timezone of installation location for 'savings'
        """
        return self.api('HISTORY_DATA', kind=kind, period=period,
                        start_date=start_date, end_date=end_date,
                        installation_timezone=installation_timezone,
                        timezone=timezone)['response']

    def command(self, name, **kwargs):
        """ Wrapper method for product command response error handling """
        response = self.api(name, **kwargs)['response']
        if response['code'] == 201:
            return response.get('message')
        raise ProductError(response.get('message'))

class BatteryTariffPeriodCost(
        namedtuple('BatteryTariffPeriodCost', ['buy', 'sell', 'name'])):
    """ Represents the costs of a tariff period
    buy: A float containing the import price
    sell: A float containing the export price
    name: The name for the period, must be 'ON_PEAK', 'PARTIAL_PEAK', 'OFF_PEAK',
    or 'SUPER_OFF_PEAK'
    """
    __slots__ = ()

class BatteryTariffPeriod(
        namedtuple('BatteryTariffPeriod', ['cost', 'start', 'end'])):
    """ Represents a time period of a tariff
    cost: A BatteryTariffPeriodCost object representing the cost for this
    time period
    start: A datetime.time object representing the start time of the period
    end: A datetime.time object representing the end time of the period
    """
    __slots__ = ()

class Battery(Product):
    """ Powerwall class """

    def get_battery_data(self):
        """ Retrieve detailed state and configuration of the battery """
        self.update(self.api('BATTERY_DATA')['response'])
        return self

    def set_operation(self, mode):
        """ Set battery operation to self_consumption, backup or autonomous """
        return self.command('BATTERY_OPERATION_MODE', default_real_mode=mode)

    def set_backup_reserve_percent(self, percent):
        """ Set the minimum backup reserve percent for that battery """
        return self.command('BACKUP_RESERVE',
                            backup_reserve_percent=int(percent))

    def set_import_export(
            self, allow_grid_charging=None, allow_battery_export=None):
        """ Sets the battery grid import and export settings
        allow_grid_charging: Optional bool argument indicating if charging from
        the grid is allowed.
        allow_battery_export: Optional bool argument indicating if export to the
        grid is allowed.
        """
        params = {}
        if allow_grid_charging is not None:
            val = not allow_grid_charging
            params['disallow_charge_from_grid_with_solar_installed'] = val
        if allow_battery_export is not None:
            val = 'battery_ok' if allow_battery_export else 'pv_only'
            params['customer_preferred_export_rule'] = val
        # This endpoint returns an empty responce instead of a result code, so
        # api() is called instead of using command()
        self.api('ENERGY_SITE_IMPORT_EXPORT_CONFIG', **params)

    def get_tariff(self):
        """ Get the tariff rate data """
        return self.api('SITE_TARIFF')['response']

    def set_tariff(self, tariff_data):
        """ Set the tariff rate data. The data can be created manually, or
        generated by create_tariff """
        return self.command('TIME_OF_USE_SETTINGS',
                            tou_settings={"tariff_content": tariff_data})

    @staticmethod
    def create_tariff(default_price, periods, provider, plan):
        """ Creates a correctly formatted dictionary of tariff data
        default_price: A BatteryTariffPeriodCost object representing the price
        of the background time period
        periods: A list of BatteryTariffPeriod objects representing times with
        higher prices than the background time period
        provider: The name of the energy provider
        plan: The name of the plan
        """
        midnight_start_time = datetime.time(hour=0)
        midnight_end_time = datetime.time(hour=23, minute=59, second=59)
        background_time = [[midnight_start_time, midnight_end_time]]

        # Subtract each of the time periods from the background time
        costs = defaultdict(list)
        for period in periods:
            slot_found = False
            # go through items in the background time searching for a slot that
            # completely encompas the period we're trying to add
            for (index, bg_period) in enumerate(background_time[:]):
                if bg_period[0] <= period.start and period.end <= bg_period[1]:
                    slot_found = True
                    # If the period matches the start/end times, then we just
                    # need to adjust the existing background time slot. Otherwise
                    # we need to split it.
                    if bg_period[0] == period.start:
                        background_time[index][0] = period.end
                    elif bg_period[1] == period.end:
                        background_time[index][1] = period.start
                    else:
                        background_time.append([period.end, bg_period[1]])
                        background_time[index][1] = period.start
            if not slot_found:
                return None
            # Update the list of prices
            costs[period.cost].append(period)

            # The loop above can leave background time slots with zero duration.
            # It's difficult to filter them out above as the list indexes can get
            # out of sync as we end up modifying the array being iterated over.
            # As a result it's easier to filter out invalid background slots now.
            background_time = list(filter(lambda t: t[0] != t[1],
                                          background_time))

        # add the background time slots to the costs array
        costs[default_price] = [BatteryTariffPeriod(default_price, x[0], x[1])
                                for x in background_time]

        tou_periods = {}
        buy_price_info = {}
        sell_price_info = {}
        for cost in sorted(costs, reverse=True):
            name = cost.name
            buy_price_info[name] = cost.buy
            sell_price_info[name] = cost.sell
            periods_for_cost = []
            for period in costs[cost]:
                # Map the second before midnight back to midnight, after
                # the time comparisons. This is required to get the json
                # in the right format
                if period.end == midnight_end_time:
                    period = period._replace(end=midnight_start_time)
                periods_for_cost.append({
                    "fromDayOfWeek": 0, "fromHour": period.start.hour,
                    "fromMinute": period.start.minute, "toDayOfWeek": 6,
                    "toHour": period.end.hour,
                    "toMinute": period.end.minute})
            tou_periods[name] = periods_for_cost

        # Build the final dict
        demand_changes = {"ALL": {"ALL": 0}, "Summer": {}, "Winter": {}}
        daily_charges = [{"name":   "Charge", "amount": 0}]
        seasons = {"Summer": {"fromMonth": 1, "fromDay": 1, "toDay": 31,
                              "toMonth": 12, "tou_periods": tou_periods},
                   "Winter": {"tou_periods": {}}}
        return JsonDict(
            daily_charges=daily_charges, demand_charges=demand_changes,
            name=plan, utility=provider, seasons=seasons,
            energy_charges={"ALL": {"ALL": 0}, "Summer": buy_price_info,
                            "Winter": {}},
            sell_tariff={"daily_charges": daily_charges,
                         "demand_charges": demand_changes, "name": plan,
                         "utility": provider, "seasons": seasons,
                         "energy_charges": {"ALL": {"ALL": 0},
                                            "Summer": sell_price_info,
                                            "Winter": {}}})

class SolarPanel(Product):
    """ Solar panel class """

    def get_site_data(self):
        """ Retrieve current site generation data """
        self.update(self.api('SITE_DATA')['response'])
        return self


from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar

import attr

if TYPE_CHECKING:
    from ..models.webhook_config import WebhookConfig
    from ..models.webhook_event_selector_type_0 import WebhookEventSelectorType0

T = TypeVar("T", bound="WebhookInfo")

@attr.s(auto_attribs=True)
class WebhookInfo:
    """
    Attributes:
        config (WebhookConfig):
        event_selector ('WebhookEventSelectorType0'):
        id (str):
    """

    config: "WebhookConfig"
    event_selector: "WebhookEventSelectorType0"
    id: str
    additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        from ..models.webhook_event_selector_type_0 import WebhookEventSelectorType0

        config = self.config.to_dict()

        event_selector: Dict[str, Any]

        if isinstance(self.event_selector, WebhookEventSelectorType0):
            event_selector = self.event_selector.to_dict()

        id = self.id

        field_dict: Dict[str, Any] = {}
        field_dict.update(self.additional_properties)
        field_dict.update(
            {
                "config": config,
                "eventSelector": event_selector,
                "id": id,
            }
        )

        return field_dict

    @classmethod
    def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
        from ..models.webhook_config import WebhookConfig
        from ..models.webhook_event_selector_type_0 import WebhookEventSelectorType0

        d = src_dict.copy()
        config = WebhookConfig.from_dict(d.pop("config"))

        def _parse_event_selector(data: object) -> "WebhookEventSelectorType0":
            if not isinstance(data, dict):
                raise TypeError()
            componentsschemas_webhook_event_selector_type_0 = WebhookEventSelectorType0.from_dict(data)

            return componentsschemas_webhook_event_selector_type_0

        event_selector = _parse_event_selector(d.pop("eventSelector"))

        id = d.pop("id")

        webhook_info = cls(
            config=config,
            event_selector=event_selector,
            id=id,
        )

        webhook_info.additional_properties = d
        return webhook_info

    @property
    def additional_keys(self) -> List[str]:
        return list(self.additional_properties.keys())

    def __getitem__(self, key: str) -> Any:
        return self.additional_properties[key]

    def __setitem__(self, key: str, value: Any) -> None:
        self.additional_properties[key] = value

    def __delitem__(self, key: str) -> None:
        del self.additional_properties[key]

    def __contains__(self, key: str) -> bool:
        return key in self.additional_properties